Greasy Fork is available in English.
单面板多标签页设计,集成智能回帖和资源采集功能。支持快速回帖、批量回帖(1-50个)、智能去重、随机回复内容;支持内容提取、图片Base64转换、文章状态检测、多账号切换、服务器上传。可拖拽面板,可最小化为圆形按钮,标签页状态记忆。
当前为
// ==UserScript==
// @name 夸克资源助手
// @namespace http://tampermonkey.net/
// @version 5.1.1
// @description 单面板多标签页设计,集成智能回帖和资源采集功能。支持快速回帖、批量回帖(1-50个)、智能去重、随机回复内容;支持内容提取、图片Base64转换、文章状态检测、多账号切换、服务器上传。可拖拽面板,可最小化为圆形按钮,标签页状态记忆。
// @match https://kuafuzys.net/*
// @match https://www.kuafuzy.com/*
// @match https://www.kuakesou.com/*
// @match https://www.kuakeq.com/*
// @match https://kuakezy.cc/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @author PYY
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ========================================
// 配置模块
// ========================================
const CONFIG = {
// 选择器配置
selectors: {
replyTextarea: '#message',
replySubmitBtn: '#submit',
threadList: 'ul.threadlist li.media.thread .style3_subject a[href^="thread-"]'
},
// 随机回复内容池
replyTemplates: [
"感谢分享,非常不错的资源!",
"太棒了,正好需要这个!",
"优秀的内容,支持楼主!",
"收藏了,感谢分享!",
"这个资源很实用,赞一个!",
"好东西,感谢楼主的分享!",
"非常感谢,辛苦了!",
"很有帮助,支持一下!"
],
// 延迟配置(毫秒)
delays: {
beforeSubmit: 800, // 提交前等待
afterSubmit: 2000, // 提交后等待
betweenPosts: 3000, // 批量回帖间隔
pageLoad: 1000 // 页面加载等待
},
// 限制配置
limits: {
maxBatchCount: 50, // 单次批量最大数量
maxLogEntries: 100, // 最大日志条数
maxPageAttempts: 30 // 最大翻页尝试
},
// 存储键名
storageKeys: {
repliedThreads: 'replied_threads_v5',
batchQueue: 'batch_queue_v5',
batchMode: 'batch_mode_v5',
batchCount: 'batch_count_v5',
logs: 'logs_v5',
statusText: 'status_text_v5',
failedAttempts: 'failed_attempts_v5',
bindCookieId: 'quark_tool_bindCookieId',
currentTab: 'current_tab_v5',
panelMinimized: 'panel_minimized_v5'
},
// 采集配置
collection: {
serverUrl: "https://zys.52huahua.cn/api/biz/collection/save",
checkUrl: "https://zys.52huahua.cn/api/biz/collection/isExist",
platform: "kkwpzys",
accounts: [
{ label: "我想我是海", value: "1896186752012374017" },
{ label: "书生", value: "1900922270486798338" },
{ label: "海海游戏社", value: "1900354501367640065" }
]
}
};
// ========================================
// 工具函数模块
// ========================================
const Utils = {
// 延迟函数
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
// 随机延迟
randomDelay: (min, max) => {
const ms = min + Math.random() * (max - min);
return Utils.delay(ms);
},
// 获取随机回复内容
getRandomReply: () => {
const templates = CONFIG.replyTemplates;
return templates[Math.floor(Math.random() * templates.length)];
},
// 解析帖子ID
parseThreadId: (url) => {
const match = url.match(/thread-(\d+)(-\d+-\d+)?\.htm/);
return match ? match[1] : null;
},
// 检查是否为帖子详情页
isThreadPage: () => {
return /\/thread-\d+(-\d+-\d+)?\.htm/.test(location.href);
},
// 检查是否为用户列表页
isUserListPage: () => {
return /\/user-thread-\d+(-\d+)?\.htm/.test(location.href);
},
// 格式化日期为 YYYY-MM-DD HH:mm:ss
formatDateTime: (date) => {
const pad = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
// XPath 辅助函数 - 获取单个元素
getElementByXPath: (xpath) => {
try {
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue;
} catch (e) {
console.error("XPath 错误:", e);
return null;
}
},
// XPath 辅助函数 - 获取所有匹配的元素
getElementsByXPath: (xpath) => {
try {
const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
const elements = [];
for (let i = 0; i < result.snapshotLength; i++) {
elements.push(result.snapshotItem(i));
}
return elements;
} catch (e) {
console.error("XPath 错误:", e);
return [];
}
}
};
// ========================================
// 存储管理模块
// ========================================
const Storage = {
// 获取已回帖列表
getRepliedThreads: () => {
return GM_getValue(CONFIG.storageKeys.repliedThreads, []) || [];
},
// 添加已回帖记录
addRepliedThread: (tid) => {
const replied = Storage.getRepliedThreads();
if (!replied.includes(tid)) {
replied.push(tid);
GM_setValue(CONFIG.storageKeys.repliedThreads, replied);
}
},
// 检查是否已回帖
isReplied: (tid) => {
return Storage.getRepliedThreads().includes(tid);
},
// 清空已回帖记录
clearRepliedThreads: () => {
GM_setValue(CONFIG.storageKeys.repliedThreads, []);
},
// 获取批量队列
getBatchQueue: () => {
return GM_getValue(CONFIG.storageKeys.batchQueue, []) || [];
},
// 保存批量队列
saveBatchQueue: (queue) => {
GM_setValue(CONFIG.storageKeys.batchQueue, queue);
},
// 获取批量模式状态
isBatchMode: () => {
return GM_getValue(CONFIG.storageKeys.batchMode, false);
},
// 设置批量模式
setBatchMode: (enabled) => {
GM_setValue(CONFIG.storageKeys.batchMode, enabled);
},
// 获取批量剩余数量
getBatchCount: () => {
return GM_getValue(CONFIG.storageKeys.batchCount, 0);
},
// 设置批量剩余数量
setBatchCount: (count) => {
GM_setValue(CONFIG.storageKeys.batchCount, count);
},
// 获取日志
getLogs: () => {
return GM_getValue(CONFIG.storageKeys.logs, []) || [];
},
// 保存日志
saveLogs: (logs) => {
GM_setValue(CONFIG.storageKeys.logs, logs);
},
// 添加日志
addLog: (message, type) => {
const logs = Storage.getLogs();
const time = new Date().toLocaleTimeString();
logs.unshift({ time, message, type });
// 限制日志数量
if (logs.length > CONFIG.limits.maxLogEntries) {
logs.pop();
}
Storage.saveLogs(logs);
},
// 清空日志
clearLogs: () => {
GM_setValue(CONFIG.storageKeys.logs, []);
},
// 获取状态文本
getStatusText: () => {
return GM_getValue(CONFIG.storageKeys.statusText, '待机中');
},
// 设置状态文本
setStatusText: (text) => {
GM_setValue(CONFIG.storageKeys.statusText, text);
},
// 获取失败尝试次数
getFailedAttempts: () => {
return GM_getValue(CONFIG.storageKeys.failedAttempts, 0);
},
// 设置失败尝试次数
setFailedAttempts: (count) => {
GM_setValue(CONFIG.storageKeys.failedAttempts, count);
},
// 重置失败尝试次数
resetFailedAttempts: () => {
GM_setValue(CONFIG.storageKeys.failedAttempts, 0);
}
};
// ========================================
// 采集数据模块
// ========================================
const CollectionData = {
data: null,
// 初始化采集数据
init: () => {
CollectionData.data = {
collectionPlatform: CONFIG.collection.platform,
resourceLink: null,
title: null,
username: null,
uid: null,
content: null,
node: null,
tags: null,
quarkLink: null,
status: "1",
createTime: Utils.formatDateTime(new Date()),
createUser: "1543837863788879871",
deleteFlag: "NOT_DELETE",
bindCookieId: localStorage.getItem(CONFIG.storageKeys.bindCookieId) || CONFIG.collection.accounts[0].value
};
},
// 获取采集数据
get: () => CollectionData.data,
// 重置采集数据
reset: () => CollectionData.init()
};
// ========================================
// UI模块
// ========================================
const UI = {
panel: null,
logContainer: null,
collectionLogArea: null,
currentTab: 'reply', // 当前激活的标签页
// 初始化样式
initStyles: () => {
GM_addStyle(`
#unifiedPanel {
position: fixed;
top: 100px;
right: 20px;
width: 380px;
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
max-height: 80vh;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
#unifiedPanel.minimized {
width: 60px;
height: 60px;
border-radius: 50%;
max-height: none;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
#unifiedPanel.minimized .panel-header {
border-radius: 50%;
padding: 0;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#unifiedPanel.minimized .panel-header h3 {
font-size: 24px;
margin: 0;
}
#unifiedPanel.minimized .panel-header > div {
display: none;
}
#unifiedPanel.minimized .tab-nav,
#unifiedPanel.minimized .tab-content {
display: none;
}
#unifiedPanel .panel-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 15px;
border-radius: 8px 8px 0 0;
cursor: move;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
#unifiedPanel .panel-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
/* 标签页导航 */
#unifiedPanel .tab-nav {
display: flex;
background: #f5f5f5;
border-bottom: 2px solid #e0e0e0;
}
#unifiedPanel .tab-nav button {
flex: 1;
padding: 12px 16px;
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #666;
transition: all 0.3s;
border-bottom: 3px solid transparent;
}
#unifiedPanel .tab-nav button:hover {
background: #e8e8e8;
color: #333;
}
#unifiedPanel .tab-nav button.active {
color: #667eea;
background: #fff;
border-bottom-color: #667eea;
}
/* 标签页内容 */
#unifiedPanel .tab-content {
display: none;
padding: 15px;
overflow-y: auto;
flex: 1;
}
#unifiedPanel .tab-content.active {
display: block;
}
/* 通用样式 */
#unifiedPanel .btn-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
#unifiedPanel button:not(.tab-nav button) {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
#unifiedPanel button.primary {
background: #667eea;
color: white;
}
#unifiedPanel button.primary:hover {
background: #5568d3;
}
#unifiedPanel button.secondary {
background: #f5f5f5;
color: #333;
}
#unifiedPanel button.secondary:hover {
background: #e8e8e8;
}
#unifiedPanel button.danger {
background: #ef5350;
color: white;
}
#unifiedPanel button.danger:hover {
background: #e53935;
}
#unifiedPanel button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#unifiedPanel .input-group {
margin-bottom: 12px;
}
#unifiedPanel input, #unifiedPanel select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 13px;
box-sizing: border-box;
}
#unifiedPanel input:focus, #unifiedPanel select:focus {
outline: none;
border-color: #667eea;
}
#unifiedPanel .divider {
height: 1px;
background: #e0e0e0;
margin: 12px 0;
}
#unifiedPanel .log-container, #unifiedPanel .log-area {
max-height: 200px;
overflow-y: auto;
background: #f9f9f9;
border-radius: 5px;
padding: 8px;
font-size: 12px;
}
#unifiedPanel .log-entry {
margin: 4px 0;
padding: 4px 6px;
border-radius: 3px;
line-height: 1.4;
}
#unifiedPanel .log-entry.info {
color: #333;
}
#unifiedPanel .log-entry.success {
color: #2e7d32;
background: #e8f5e9;
}
#unifiedPanel .log-entry.error {
color: #c62828;
background: #ffebee;
}
#unifiedPanel .log-entry .time {
color: #999;
font-size: 11px;
margin-right: 6px;
}
#unifiedPanel .status-bar, #unifiedPanel .status-container {
padding: 8px 12px;
background: #f5f5f5;
border-radius: 5px;
margin-bottom: 12px;
font-size: 12px;
color: #666;
}
#unifiedPanel .status-bar .label {
font-weight: 600;
color: #333;
}
#unifiedPanel .status-light {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ccc;
display: inline-block;
margin-left: 10px;
vertical-align: middle;
transition: background 0.3s ease;
}
#unifiedPanel .status-text {
margin-left: 5px;
vertical-align: middle;
font-size: 12px;
color: #666;
}
`);
},
// 创建统一面板
createPanel: () => {
const panel = document.createElement('div');
panel.id = 'unifiedPanel';
panel.innerHTML = `
<div class="panel-header">
<h3>🚀 夸克资源助手</h3>
<div>
<span style="cursor: pointer; margin-right: 10px;" id="panelMinimize">−</span>
<span style="cursor: pointer;" id="panelClose">✕</span>
</div>
</div>
<!-- 标签页导航 -->
<div class="tab-nav">
<button class="tab-btn active" data-tab="reply">💬 回帖</button>
<button class="tab-btn" data-tab="collection">📦 采集</button>
</div>
<!-- 回帖标签页 -->
<div class="tab-content active" id="tab-reply">
<div class="status-bar">
<span class="label">状态:</span><span id="statusText">待机中</span>
</div>
<div class="input-group">
<input type="number" id="userIdInput" placeholder="输入用户ID">
</div>
<div class="btn-group">
<button class="secondary" id="btnGoToUser">跳转列表</button>
<button class="primary" id="btnQuickReply">快速回帖</button>
</div>
<div class="divider"></div>
<div class="btn-group">
<button class="secondary" id="btnBatchReply">批量回帖</button>
</div>
<div class="input-group" id="batchInputGroup" style="display:none;">
<input type="number" id="batchCount" placeholder="输入批量回帖数量 (1-50)" min="1" max="50">
</div>
<div class="btn-group" id="batchControlGroup" style="display:none;">
<button class="primary" id="btnStartBatch">开始批量</button>
<button class="danger" id="btnStopBatch">停止</button>
</div>
<div class="divider"></div>
<div class="btn-group">
<button class="secondary" id="btnClearHistory">清空记录</button>
<button class="secondary" id="btnViewStats">查看统计</button>
</div>
<div class="divider"></div>
<div class="log-container" id="logContainer"></div>
</div>
<!-- 采集标签页 -->
<div class="tab-content" id="tab-collection">
<div class="status-container">
<strong>文章状态:</strong>
<div class="status-light" id="status-light"></div>
<span class="status-text" id="status-text">未检查</span>
</div>
<div class="input-group">
<label style="display:block;margin-bottom:5px;font-weight:500;">绑定账号</label>
<select id="account-selector"></select>
</div>
<div class="divider"></div>
<div class="btn-group">
<button class="primary" id="btnQuickReply2">快速回帖</button>
<button class="primary" id="btnExtract">提取内容</button>
</div>
<div class="btn-group">
<button class="secondary" id="btnShowData">查看数据</button>
<button class="secondary" id="btnUpload">上传服务器</button>
</div>
<div class="divider"></div>
<div class="log-area" id="collectionLogArea"></div>
</div>
`;
document.body.appendChild(panel);
UI.panel = panel;
UI.logContainer = panel.querySelector('#logContainer');
UI.collectionLogArea = panel.querySelector('#collectionLogArea');
// 恢复上次使用的标签页
const savedTab = GM_getValue(CONFIG.storageKeys.currentTab, 'reply');
UI.currentTab = savedTab;
// 如果保存的不是默认标签,需要切换
if (savedTab !== 'reply') {
const tabBtns = panel.querySelectorAll('.tab-nav button');
const tabContents = panel.querySelectorAll('.tab-content');
tabBtns.forEach(btn => {
if (btn.getAttribute('data-tab') === savedTab) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
tabContents.forEach(content => {
if (content.id === `tab-${savedTab}`) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
}
// 恢复最小化状态
const isMinimized = GM_getValue(CONFIG.storageKeys.panelMinimized, false);
if (isMinimized) {
panel.classList.add('minimized');
// 最小化状态下按钮会被隐藏,所以不需要改变文本
}
// 初始化采集账号选择器
UI.initAccountSelector();
// 绑定事件
UI.bindEvents();
UI.bindTabEvents();
// 使面板可拖拽
UI.makeDraggable(panel, panel.querySelector('.panel-header'));
},
// 绑定事件
bindEvents: () => {
// 跳转到用户列表页
document.getElementById('btnGoToUser').onclick = () => {
const userId = document.getElementById('userIdInput').value.trim();
if (!userId) {
UI.log('请输入用户ID', 'error');
return;
}
if (!/^\d+$/.test(userId)) {
UI.log('用户ID必须是数字', 'error');
return;
}
UI.log(`跳转到用户 ${userId} 的帖子列表`, 'info');
// 使用当前域名而不是硬编码
const currentOrigin = window.location.origin;
location.href = `${currentOrigin}/user-thread-${userId}.htm`;
};
// 快速回帖按钮
document.getElementById('btnQuickReply').onclick = () => {
ReplyHandler.quickReply();
};
// 批量回帖按钮
document.getElementById('btnBatchReply').onclick = () => {
UI.toggleBatchMode();
};
// 开始批量
document.getElementById('btnStartBatch').onclick = () => {
const count = parseInt(document.getElementById('batchCount').value);
if (!count || count < 1 || count > CONFIG.limits.maxBatchCount) {
UI.log(`请输入有效的数量 (1-${CONFIG.limits.maxBatchCount})`, 'error');
return;
}
ReplyHandler.startBatch(count);
};
// 停止批量
document.getElementById('btnStopBatch').onclick = () => {
ReplyHandler.stopBatch();
};
// 清空记录
document.getElementById('btnClearHistory').onclick = () => {
if (confirm('确定要清空所有回帖记录、日志和队列吗?')) {
Storage.clearRepliedThreads();
Storage.clearLogs();
Storage.saveBatchQueue([]);
Storage.setBatchMode(false);
Storage.setBatchCount(0);
Storage.resetFailedAttempts();
if (UI.logContainer) {
UI.logContainer.innerHTML = '';
}
UI.log('已清空所有记录', 'success');
UI.updateStatus('待机中');
UI.setButtonsDisabled(false);
}
};
// 查看统计
document.getElementById('btnViewStats').onclick = () => {
const replied = Storage.getRepliedThreads();
UI.log(`已回帖数量:${replied.length} 个`, 'info');
};
// 最小化/最大化面板
const toggleMinimize = (forceRestore = false) => {
const isMinimized = UI.panel.classList.contains('minimized');
const minimizeBtn = document.getElementById('panelMinimize');
if (isMinimized || forceRestore) {
// 恢复 - 重置位置到右上角避免溢出
UI.panel.classList.remove('minimized');
UI.panel.style.top = '100px';
UI.panel.style.right = '20px';
UI.panel.style.left = 'auto';
if (minimizeBtn) minimizeBtn.textContent = '−';
GM_setValue(CONFIG.storageKeys.panelMinimized, false);
} else {
// 最小化
UI.panel.classList.add('minimized');
if (minimizeBtn) minimizeBtn.textContent = '−';
GM_setValue(CONFIG.storageKeys.panelMinimized, true);
}
};
// 最小化按钮点击
document.getElementById('panelMinimize').onclick = (e) => {
e.stopPropagation();
toggleMinimize();
};
// 最小化状态下点击面板头部恢复(但拖拽时不展开)
UI.panel.querySelector('.panel-header').addEventListener('click', (e) => {
if (UI.panel.classList.contains('minimized')) {
// 检查是否刚刚拖拽过
if (UI.panel._hasMoved && UI.panel._hasMoved()) {
return; // 如果是拖拽,不展开
}
e.stopPropagation();
toggleMinimize(true);
}
});
// 关闭面板
document.getElementById('panelClose').onclick = () => {
UI.panel.style.display = 'none';
};
// 采集功能按钮
document.getElementById('btnQuickReply2').onclick = () => ReplyHandler.quickReply();
document.getElementById('btnExtract').onclick = () => Collector.extractAll();
document.getElementById('btnUpload').onclick = () => Collector.uploadServer();
document.getElementById('btnShowData').onclick = () => Collector.showData();
},
// 绑定标签页切换事件
bindTabEvents: () => {
const tabBtns = UI.panel.querySelectorAll('.tab-nav button');
const tabContents = UI.panel.querySelectorAll('.tab-content');
console.log('绑定标签页事件,找到按钮数量:', tabBtns.length);
tabBtns.forEach((btn, index) => {
console.log(`按钮${index}:`, btn.getAttribute('data-tab'));
btn.addEventListener('click', (e) => {
e.preventDefault();
const targetTab = btn.getAttribute('data-tab');
console.log('点击标签:', targetTab);
// 移除所有active类
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// 添加active类到当前标签
btn.classList.add('active');
const targetContent = document.getElementById(`tab-${targetTab}`);
if (targetContent) {
targetContent.classList.add('active');
console.log('切换到标签:', targetTab);
} else {
console.error('未找到标签内容:', `tab-${targetTab}`);
}
// 保存当前标签到本地存储
UI.currentTab = targetTab;
GM_setValue(CONFIG.storageKeys.currentTab, targetTab);
});
});
},
// 初始化账号选择器
initAccountSelector: () => {
const accountSelect = document.getElementById('account-selector');
if (!accountSelect) return;
CONFIG.collection.accounts.forEach(({ label, value }) => {
const option = document.createElement('option');
option.textContent = label;
option.value = value;
accountSelect.appendChild(option);
});
const savedBindCookieId = localStorage.getItem(CONFIG.storageKeys.bindCookieId);
const isValid = CONFIG.collection.accounts.some(acc => acc.value === savedBindCookieId);
accountSelect.value = isValid ? savedBindCookieId : CONFIG.collection.accounts[0].value;
// 确保 CollectionData.data 已初始化
if (CollectionData.data) {
CollectionData.data.bindCookieId = accountSelect.value;
}
accountSelect.addEventListener('change', (e) => {
if (CollectionData.data) {
CollectionData.data.bindCookieId = e.target.value;
}
localStorage.setItem(CONFIG.storageKeys.bindCookieId, e.target.value);
UI.addCollectionLog('已切换到账号: ' + e.target.options[e.target.selectedIndex].text);
});
},
// 切换批量模式UI
toggleBatchMode: () => {
const inputGroup = document.getElementById('batchInputGroup');
const controlGroup = document.getElementById('batchControlGroup');
const isVisible = inputGroup.style.display !== 'none';
inputGroup.style.display = isVisible ? 'none' : 'block';
controlGroup.style.display = isVisible ? 'none' : 'flex';
},
// 使面板可拖拽
makeDraggable: () => {
const header = UI.panel.querySelector('.panel-header');
let isDragging = false;
let hasMoved = false;
let currentX, currentY, initialX, initialY;
header.addEventListener('mousedown', (e) => {
if (e.target.id === 'panelClose' || e.target.id === 'panelMinimize') return;
isDragging = true;
hasMoved = false;
initialX = e.clientX - UI.panel.offsetLeft;
initialY = e.clientY - UI.panel.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
hasMoved = true;
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
UI.panel.style.left = currentX + 'px';
UI.panel.style.top = currentY + 'px';
UI.panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
// 重置移动标记,延迟一点以便点击事件能检测到
setTimeout(() => {
hasMoved = false;
}, 100);
});
// 保存 hasMoved 状态供点击事件使用
UI.panel._hasMoved = () => hasMoved;
},
// 记录日志
log: (message, type = 'info') => {
// 保存到存储
Storage.addLog(message, type);
// 显示到UI
if (UI.logContainer) {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const time = new Date().toLocaleTimeString();
entry.innerHTML = `<span class="time">${time}</span>${message}`;
UI.logContainer.insertBefore(entry, UI.logContainer.firstChild);
// 限制日志数量
const entries = UI.logContainer.querySelectorAll('.log-entry');
if (entries.length > CONFIG.limits.maxLogEntries) {
entries[entries.length - 1].remove();
}
}
console.log(`[回帖助手] ${message}`);
},
// 更新状态
updateStatus: (text) => {
// 保存到存储
Storage.setStatusText(text);
// 显示到UI
const statusText = document.getElementById('statusText');
if (statusText) {
statusText.textContent = text;
}
},
// 恢复日志
restoreLogs: () => {
const logs = Storage.getLogs();
if (UI.logContainer && logs.length > 0) {
UI.logContainer.innerHTML = '';
logs.forEach(log => {
const entry = document.createElement('div');
entry.className = `log-entry ${log.type}`;
entry.innerHTML = `<span class="time">${log.time}</span>${log.message}`;
UI.logContainer.appendChild(entry);
});
}
},
// 恢复状态
restoreStatus: () => {
const statusText = Storage.getStatusText();
UI.updateStatus(statusText);
},
// 禁用/启用按钮
setButtonsDisabled: (disabled) => {
const buttons = UI.panel.querySelectorAll('button');
buttons.forEach(btn => {
if (btn.id !== 'btnStopBatch') {
btn.disabled = disabled;
}
});
},
// 添加采集日志
addCollectionLog: (msg) => {
console.log(`[采集工具] ${msg}`);
if (UI.collectionLogArea) {
const p = document.createElement('div');
p.textContent = msg;
UI.collectionLogArea.appendChild(p);
UI.collectionLogArea.scrollTop = UI.collectionLogArea.scrollHeight;
}
},
// 更新状态灯
updateStatusLight: (color, text) => {
const light = document.getElementById('status-light');
const textSpan = document.getElementById('status-text');
if (light) light.style.background = color;
if (textSpan) textSpan.textContent = text;
}
};
// ========================================
// 回帖处理模块
// ========================================
const ReplyHandler = {
// 快速回帖(当前页面)
quickReply: async () => {
if (!Utils.isThreadPage()) {
UI.log('请在帖子详情页使用快速回帖功能', 'error');
return;
}
const tid = Utils.parseThreadId(location.href);
if (!tid) {
UI.log('无法解析帖子ID', 'error');
return;
}
if (Storage.isReplied(tid)) {
UI.log('该帖子已回复过,跳过', 'error');
return;
}
UI.updateStatus('正在回帖...');
UI.setButtonsDisabled(true);
try {
await ReplyHandler.submitReply(tid);
UI.log('回帖成功!', 'success');
UI.updateStatus('回帖完成');
} catch (error) {
UI.log(`回帖失败:${error.message}`, 'error');
UI.updateStatus('回帖失败');
} finally {
UI.setButtonsDisabled(false);
}
},
// 提交回复
submitReply: async (tid) => {
const textarea = document.querySelector(CONFIG.selectors.replyTextarea);
const submitBtn = document.querySelector(CONFIG.selectors.replySubmitBtn);
if (!textarea || !submitBtn) {
throw new Error('未找到回复框或提交按钮');
}
// 填充随机内容
const replyText = Utils.getRandomReply();
textarea.value = replyText;
// 触发事件
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
UI.log(`回复内容:${replyText}`, 'info');
// 等待后提交
await Utils.delay(CONFIG.delays.beforeSubmit);
submitBtn.click();
// 标记已回复
Storage.addRepliedThread(tid);
// 等待提交完成
await Utils.delay(CONFIG.delays.afterSubmit);
},
// 开始批量回帖
startBatch: async (count) => {
if (!Utils.isUserListPage()) {
UI.log('请在用户帖子列表页使用批量回帖功能', 'error');
return;
}
// 获取所有未回复的帖子
const threadLinks = document.querySelectorAll(CONFIG.selectors.threadList);
const unrepliedLinks = Array.from(threadLinks)
.map(link => ({
url: link.href,
tid: Utils.parseThreadId(link.href)
}))
.filter(item => item.tid && !Storage.isReplied(item.tid));
if (unrepliedLinks.length === 0) {
UI.log('当前页面没有未回复的帖子', 'error');
return;
}
// 从所有未回复的帖子中随机选择 count 个
const shuffled = unrepliedLinks.sort(() => Math.random() - 0.5);
const targetLinks = shuffled.slice(0, Math.min(count, unrepliedLinks.length));
const queue = targetLinks.map(item => item.url);
// 保存队列
Storage.saveBatchQueue(queue);
Storage.setBatchMode(true);
Storage.setBatchCount(queue.length);
Storage.resetFailedAttempts();
UI.log(`从 ${unrepliedLinks.length} 个未回复帖子中随机选择了 ${queue.length} 个`, 'success');
UI.log(`开始批量回帖,队列中有 ${queue.length} 个帖子`, 'success');
UI.updateStatus(`批量模式:剩余 ${queue.length} 个帖子`);
UI.setButtonsDisabled(true);
await ReplyHandler.processBatch();
},
// 处理批量回帖
processBatch: async () => {
if (!Storage.isBatchMode()) {
return;
}
// 从队列获取下一个帖子
let queue = Storage.getBatchQueue();
if (queue.length === 0) {
UI.log('🎉 批量回帖全部完成!', 'success');
ReplyHandler.stopBatch();
return;
}
// 取第一个(队列已在startBatch时随机打乱)
const nextUrl = queue[0];
const tid = Utils.parseThreadId(nextUrl);
UI.log(`→ 准备回复帖子:${tid} (队列剩余 ${queue.length})`, 'info');
UI.updateStatus(`批量模式:剩余 ${queue.length} 个帖子`);
// 从队列中移除第一个(访问前就删除,避免重复)
queue.shift();
Storage.saveBatchQueue(queue);
Storage.setBatchCount(queue.length);
// 跳转到帖子页面
location.href = nextUrl;
},
// 停止批量回帖
stopBatch: () => {
Storage.setBatchMode(false);
Storage.setBatchCount(0);
Storage.saveBatchQueue([]);
Storage.resetFailedAttempts();
UI.log('已停止批量回帖', 'success');
UI.updateStatus('待机中');
UI.setButtonsDisabled(false);
},
// 在帖子页面自动回帖(批量模式)
autoReplyInThread: async () => {
if (!Storage.isBatchMode()) return;
const tid = Utils.parseThreadId(location.href);
if (!tid) {
UI.log('无法解析帖子ID', 'error');
return;
}
if (Storage.isReplied(tid)) {
UI.log(`帖子 ${tid} 已回复过,跳过`, 'info');
await Utils.delay(1000);
// 已经从队列中移除了,直接返回继续下一个
history.back();
return;
}
UI.updateStatus('正在自动回帖...');
try {
await Utils.delay(CONFIG.delays.pageLoad);
await ReplyHandler.submitReply(tid);
const remaining = Storage.getBatchCount();
UI.log(`✓ 帖子 ${tid} 回复成功,剩余 ${remaining} 个帖子`, 'success');
UI.updateStatus(`批量模式:剩余 ${remaining} 个帖子`);
// 等待后返回列表
await Utils.delay(CONFIG.delays.betweenPosts);
history.back();
} catch (error) {
UI.log(`自动回帖失败:${error.message}`, 'error');
// 出错也返回继续下一个(已从队列移除)
await Utils.delay(2000);
history.back();
}
}
};
// ========================================
// 采集处理模块
// ========================================
const Collector = {
// 检查文章是否已存在
checkArticleExists: async () => {
if (!CollectionData.data.title) {
UI.updateStatusLight('gray', '未检查');
return false;
}
UI.updateStatusLight('#FFA500', '检查中...');
try {
const response = await fetch(CONFIG.collection.checkUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: CollectionData.data.title
});
const data = await response.json();
const exists = data.data === true || data.data === 'true' || data.data === 1 || data.data === '1';
if (exists) {
UI.updateStatusLight('#f44336', '文章已存在');
UI.addCollectionLog('⚠️ 该文章已在数据库中');
return true;
} else {
UI.updateStatusLight('#4CAF50', '文章不存在');
UI.addCollectionLog('✅ 该文章为新内容');
return false;
}
} catch (err) {
UI.updateStatusLight('#FF9800', '检查失败');
UI.addCollectionLog('❌ 检查接口失败: ' + err.message);
return false;
}
},
// 统一提取所有内容
extractAll: async () => {
UI.addCollectionLog('开始提取所有内容...');
// 第一步:检查夸克链接
UI.addCollectionLog('1. 检查夸克链接...');
const alertDiv = document.querySelector("div.alert.alert-success[role='alert']");
if (alertDiv) {
const allText = alertDiv.textContent || alertDiv.innerText || '';
const quarkPattern = /https?:\/\/pan\.quark\.(cn|com)\/s\/[a-zA-Z0-9]+/g;
const matches = allText.match(quarkPattern);
if (matches && matches.length > 0) {
CollectionData.data.quarkLink = matches[0];
UI.addCollectionLog('✓ 夸克链接提取成功: ' + CollectionData.data.quarkLink);
} else {
UI.addCollectionLog('❌ 未找到夸克链接。请确认已回帖。');
return;
}
} else {
UI.addCollectionLog('❌ 未找到回帖提示框。请先回帖查看链接。');
return;
}
// 第二步:提取基本信息
UI.addCollectionLog('2. 提取标题、作者、节点和资源链接...');
await Collector.extractMeta();
// 第三步:提取标签
UI.addCollectionLog('3. 提取标签...');
Collector.extractTags();
// 第四步:提取正文
UI.addCollectionLog('4. 提取正文...');
await Collector.extractContent();
UI.addCollectionLog('✅ 所有内容提取完成!');
UI.addCollectionLog('可以点击【查看数据】查看完整数据,然后点击【上传服务器】');
},
// 提取元数据
extractMeta: async () => {
const currentUrl = window.location.href;
try {
const urlObj = new URL(currentUrl);
const pathParts = urlObj.pathname.split('/').filter(part => part);
if (pathParts.length > 0) {
CollectionData.data.resourceLink = pathParts[pathParts.length - 1];
}
UI.addCollectionLog('资源链接: ' + CollectionData.data.resourceLink);
} catch (e) {
UI.addCollectionLog('URL 解析失败: ' + e.message);
}
// 提取标题
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
CollectionData.data.title = titleEl.textContent.trim().replace(/\s+/g, " ");
UI.addCollectionLog('标题: ' + CollectionData.data.title);
} else {
UI.addCollectionLog('未找到标题');
}
// 提取作者
const userEl = document.querySelector("span.username.font-weight-bold.small a");
if (userEl) {
CollectionData.data.username = userEl.textContent.trim();
UI.addCollectionLog('作者: ' + CollectionData.data.username);
} else {
UI.addCollectionLog('未找到作者');
}
// 提取节点
const nodeEl = Utils.getElementByXPath("//*[@id='body']/div/div/div[2]/ol/li[2]/a");
if (nodeEl) {
CollectionData.data.node = nodeEl.textContent.trim();
UI.addCollectionLog('节点: ' + CollectionData.data.node);
} else {
UI.addCollectionLog('未找到节点');
}
},
// 提取标签
extractTags: () => {
const tagsXPath = "/html/body/main/div/div/div[2]/div[1]/div[2]/div[2]//a";
const tagElements = Utils.getElementsByXPath(tagsXPath);
if (tagElements && tagElements.length > 0) {
const tagTexts = tagElements.map(tag => tag.textContent.trim()).filter(text => text);
CollectionData.data.tags = tagTexts.join(",");
UI.addCollectionLog('标签: ' + CollectionData.data.tags);
} else {
UI.addCollectionLog('未找到标签');
}
},
// 提取正文内容
extractContent: async () => {
const contentXPath = "/html/body/main/div/div/div[2]/div[1]/div[2]";
const contentEl = Utils.getElementByXPath(contentXPath);
if (!contentEl) {
UI.addCollectionLog('未找到正文区域');
return;
}
const clonedContent = contentEl.cloneNode(true);
// 删除多余元素
try {
let deleteCount = 0;
const removeList = ['.tt-license', '.alert.alert-success', '.mt-3'];
removeList.forEach(sel => {
const el = clonedContent.querySelector(sel);
if (el && el.parentNode) {
el.parentNode.removeChild(el);
deleteCount++;
}
});
UI.addCollectionLog(`已删除 ${deleteCount} 个指定元素`);
} catch (e) {
UI.addCollectionLog('删除元素时出错: ' + e.message);
}
// 处理图片转Base64
const imgEls = clonedContent.querySelectorAll("img");
let converted = 0;
const convertToBase64 = async (url) => {
try {
const response = await fetch(url);
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (err) {
console.error("图片转Base64失败:", err);
return url;
}
};
const tasks = Array.from(imgEls).map(async (img) => {
const src = img.getAttribute("src");
if (!src) return;
try {
const absoluteUrl = new URL(src, window.location.href).href;
const base64 = await convertToBase64(absoluteUrl);
img.setAttribute("src", base64);
converted++;
} catch (e) {
console.warn("处理图片失败:", src, e);
}
});
await Promise.all(tasks);
UI.addCollectionLog(`共处理图片 ${imgEls.length} 张,成功转为Base64:${converted} 张`);
CollectionData.data.content = clonedContent.outerHTML;
UI.addCollectionLog('✅ 正文提取完成');
},
// 上传到服务器
uploadServer: () => {
if (!CONFIG.collection.serverUrl.startsWith("http")) {
UI.addCollectionLog('❌ 请先设置服务器地址!');
return;
}
UI.addCollectionLog('开始上传到服务器...');
fetch(CONFIG.collection.serverUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(CollectionData.data)
})
.then(res => res.json())
.then(data => UI.addCollectionLog('✅ 上传成功: ' + JSON.stringify(data)))
.catch(err => UI.addCollectionLog('❌ 上传失败: ' + err));
},
// 查看数据
showData: () => {
UI.addCollectionLog('当前收集数据:');
UI.addCollectionLog(JSON.stringify(CollectionData.data, null, 2));
}
};
// ========================================
// 主程序初始化
// ========================================
const App = {
init: async () => {
// 初始化采集数据(必须在创建面板之前)
CollectionData.init();
// 初始化UI
UI.initStyles();
UI.createPanel();
// 恢复日志和状态
UI.restoreLogs();
UI.restoreStatus();
// 如果是批量模式,显示批量控制按钮
if (Storage.isBatchMode()) {
const inputGroup = document.getElementById('batchInputGroup');
const controlGroup = document.getElementById('batchControlGroup');
if (inputGroup && controlGroup) {
inputGroup.style.display = 'block';
controlGroup.style.display = 'flex';
}
UI.setButtonsDisabled(true);
}
UI.log('夸克资源助手已启动 v5.1.1', 'success');
// 检查当前页面类型
if (Utils.isThreadPage()) {
UI.log('检测到帖子详情页', 'info');
UI.addCollectionLog('✅ 采集工具已就绪');
// 自动检查文章状态
App.autoCheckArticle();
// 如果是批量模式,自动回帖
if (Storage.isBatchMode()) {
await ReplyHandler.autoReplyInThread();
} else {
UI.updateStatus('帖子详情页 - 可使用快速回帖');
}
} else if (Utils.isUserListPage()) {
UI.log('检测到用户列表页', 'info');
if (!Storage.isBatchMode()) {
UI.updateStatus('用户列表页 - 可使用批量回帖');
} else {
// 批量模式下,在列表页继续处理
UI.log('批量模式中,准备处理下一个帖子...', 'info');
setTimeout(() => {
ReplyHandler.processBatch();
}, 1500);
}
} else {
UI.log('当前页面类型未知', 'info');
if (!Storage.isBatchMode()) {
UI.updateStatus('待机中');
}
}
},
// 自动检查文章
autoCheckArticle: () => {
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
const title = titleEl.textContent.trim().replace(/\s+/g, " ");
CollectionData.data.title = title;
Collector.checkArticleExists();
} else {
const checkObserver = new MutationObserver(() => {
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
const title = titleEl.textContent.trim().replace(/\s+/g, " ");
CollectionData.data.title = title;
Collector.checkArticleExists();
checkObserver.disconnect();
}
});
checkObserver.observe(document.body, { childList: true, subtree: true });
}
}
};
// 启动应用
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', App.init);
} else {
App.init();
}
})();