// ==UserScript== // @name 聚影(jying.top)自动签到 // @namespace https://www.jying.top/ // @version 1.0.3 // @author OpenClaw Agent // @description 聚影网盘自动签到脚本,支持后台定时运行、账号密码保存、签到通知。无需手动登录,配置一次即可自动运行。 // @license MIT // @icon https://www.jying.top/favicon.ico // @supportURL https://github.com/openclaw/jying-checkin/issues // @homepageURL https://scriptcat.org/zh-CN/script-show-page/ // @match https://www.jying.top/* // @match https://jying.top/* // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM_getResourceText // @connect www.jying.top // @connect jying.top // @tag 签到 // @tag 自动化 // @tag 效率 // @crontab 5 16 * * * once // @cloudcat true // ==/UserScript== /** * 聚影自动签到脚本 - 公众版 v1.0.3 * * 更新日志: * - v1.0.3: 添加 @tag 标签(签到/自动化/效率),添加 @homepageURL * - v1.0.2: 添加首次访问弹出配置面板,修正定时任务时间(北京时间 00:05) * - v1.0.1: 符合 ScriptCat 后台脚本规范(返回 Promise) * - v1.0.0: 初始版本 * * ScriptCat 后台脚本规范: * - 必须返回 Promise * - 不使用 IIFE * - 不使用 @run-at */ // ═══════════════════════════════════════════════════════════ // 配置常量 // ═══════════════════════════════════════════════════════════ const SITE_URL = 'https://www.jying.top'; const API = { login: '/api/app/login/', stats: '/api/app/checkin/stats/', checkin: '/api/app/checkin/do/', csrf: '/api/csrf/' }; const STORAGE = { username: 'jying_username', password: 'jying_password_enc', token: 'jying_token', tokenExpiry: 'jying_token_expiry', lastCheckin: 'jying_last_checkin', configShown: 'jying_config_shown' // 是否已显示过配置提示 }; const RETRY = { maxRetries: 2, delayMs: 2000 }; // ═══════════════════════════════════════════════════════════ // 工具函数 // ═══════════════════════════════════════════════════════════ function b64enc(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return btoa(str); } } function b64dec(str) { try { return decodeURIComponent(escape(atob(str))); } catch (e) { return atob(str); } } function save(key, value) { try { GM_setValue(key, value); return true; } catch (e) { console.error('[聚影签到] 保存失败:', key, e); return false; } } function load(key) { try { return GM_getValue(key, null); } catch (e) { console.error('[聚影签到] 读取失败:', key, e); return null; } } function remove(key) { try { GM_deleteValue(key); } catch (e) { console.error('[聚影签到] 删除失败:', key, e); } } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function notify(title, text, timeout = 5000) { console.log(`[聚影签到] ${title}: ${text}`); try { GM_notification({ title, text, timeout }); } catch (e) { console.error('[聚影签到] 通知发送失败:', e); } } function maskAccount(account) { if (!account) return '未配置'; if (account.includes('@')) { const [local, domain] = account.split('@'); return local.substring(0, 2) + '***@' + domain; } return account.substring(0, 2) + '***'; } async function retryAsync(fn, maxRetries = RETRY.maxRetries, delayMs = RETRY.delayMs) { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (e) { lastError = e; if (i < maxRetries - 1) { console.warn(`[聚影签到] 第 ${i + 1} 次失败,${delayMs}ms 后重试:`, e.message); await delay(delayMs); } } } throw lastError; } // ═══════════════════════════════════════════════════════════ // 配置面板 UI // ═══════════════════════════════════════════════════════════ function createConfigPanel() { // 添加样式 const style = document.createElement('style'); style.textContent = ` .jying-config-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999999; display: flex; align-items: center; justify-content: center; } .jying-config-panel { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 400px; width: 90%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .jying-config-title { font-size: 20px; font-weight: bold; margin-bottom: 16px; color: #333; display: flex; align-items: center; gap: 8px; } .jying-config-title::before { content: "🎉"; font-size: 24px; } .jying-config-desc { color: #666; font-size: 14px; margin-bottom: 20px; line-height: 1.5; } .jying-config-field { margin-bottom: 16px; } .jying-config-label { display: block; font-size: 14px; font-weight: 500; color: #333; margin-bottom: 6px; } .jying-config-input { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s; } .jying-config-input:focus { outline: none; border-color: #4CAF50; } .jying-config-buttons { display: flex; gap: 12px; margin-top: 20px; } .jying-config-btn { flex: 1; padding: 10px 16px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s; } .jying-config-btn-primary { background: #4CAF50; color: white; } .jying-config-btn-primary:hover { background: #45a049; } .jying-config-btn-secondary { background: #f5f5f5; color: #666; } .jying-config-btn-secondary:hover { background: #e0e0e0; } .jying-config-footer { margin-top: 16px; padding-top: 16px; border-top: 1px solid #eee; color: #999; font-size: 12px; text-align: center; } `; document.head.appendChild(style); // 创建面板 const overlay = document.createElement('div'); overlay.className = 'jying-config-overlay'; overlay.innerHTML = `
`; document.body.appendChild(overlay); // 绑定事件 const usernameInput = overlay.querySelector('#jying-username'); const passwordInput = overlay.querySelector('#jying-password'); const cancelBtn = overlay.querySelector('#jying-cancel'); const saveBtn = overlay.querySelector('#jying-save'); // 自动聚焦 setTimeout(() => usernameInput.focus(), 100); // 取消按钮 cancelBtn.addEventListener('click', () => { overlay.remove(); save(STORAGE.configShown, true); // 标记已显示过 }); // 保存按钮 saveBtn.addEventListener('click', () => { const username = usernameInput.value.trim(); const password = passwordInput.value.trim(); if (!username || !password) { notify('❌ 配置失败', '账号和密码不能为空'); usernameInput.focus(); return; } if (saveAccount(username, password)) { overlay.remove(); save(STORAGE.configShown, true); notify('✅ 配置成功', `账号: ${maskAccount(username)}\n每天 00:05 自动签到`); // 立即测试签到 main(); } else { notify('❌ 配置失败', '请检查输入是否正确'); } }); // 回车键提交 passwordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { saveBtn.click(); } }); // ESC 键关闭 document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); save(STORAGE.configShown, true); document.removeEventListener('keydown', escHandler); } }); // 点击遮罩关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); save(STORAGE.configShown, true); } }); } function showConfigPanelIfNeed() { // 如果已配置账号,不显示 if (isConfigured()) { console.log('[聚影签到] 已配置账号,不显示配置面板'); return false; } // 如果本次会话已显示过,不重复显示 if (load(STORAGE.configShown)) { console.log('[聚影签到] 本次会话已显示过配置提示'); return false; } // 显示配置面板 console.log('[聚影签到] 显示配置面板'); createConfigPanel(); return true; } // ═══════════════════════════════════════════════════════════ // 配置管理 // ═══════════════════════════════════════════════════════════ function getAccount() { const username = load(STORAGE.username); const passwordEnc = load(STORAGE.password); if (!username || !passwordEnc) return null; try { const password = b64dec(passwordEnc); return { username, password }; } catch (e) { console.error('[聚影签到] 密码解密失败:', e); return null; } } function saveAccount(username, password) { if (!username || !password) { console.error('[聚影签到] 账号密码不能为空'); return false; } save(STORAGE.username, username.trim()); save(STORAGE.password, b64enc(password.trim())); save(STORAGE.token, null); save(STORAGE.tokenExpiry, null); console.log('[聚影签到] 账号已保存:', maskAccount(username)); return true; } function isConfigured() { return getAccount() !== null; } function openConfigPanel() { createConfigPanel(); } function clearConfig() { remove(STORAGE.username); remove(STORAGE.password); remove(STORAGE.token); remove(STORAGE.tokenExpiry); remove(STORAGE.lastCheckin); remove(STORAGE.configShown); notify('✅ 配置已清除', '请重新配置账号密码'); console.log('[聚影签到] 所有配置已清除'); } // ═══════════════════════════════════════════════════════════ // API 交互 // ═══════════════════════════════════════════════════════════ function request(method, path, data = null, token = null) { return new Promise((resolve, reject) => { const url = SITE_URL + path; const headers = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }; if (token) headers['X-App-User-Token'] = token; const options = { method: method, url: url, headers: headers, timeout: 15000, onload: function(response) { console.log(`[聚影签到] ${method} ${path} → ${response.status}`); if (response.status >= 200 && response.status < 300) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(new Error('响应解析失败: ' + e.message)); } } else { try { const json = JSON.parse(response.responseText); reject(new Error(json.message || json.msg || `HTTP ${response.status}`)); } catch (e) { reject(new Error(`HTTP ${response.status}`)); } } }, onerror: function(error) { console.error('[聚影签到] 请求失败:', error); reject(new Error('网络请求失败')); }, ontimeout: function() { reject(new Error('请求超时')); } }; if (data) options.data = JSON.stringify(data); try { options.withCredentials = true; } catch (e) {} console.log(`[聚影签到] 请求: ${method} ${url}`); GM_xmlhttpRequest(options); }); } async function getCSRF() { try { await request('GET', API.csrf); console.log('[聚影签到] CSRF token 已获取'); return true; } catch (e) { console.warn('[聚影签到] CSRF 获取失败(可能不需要):', e.message); return false; } } async function login(username, password) { try { await getCSRF(); const result = await request('POST', API.login, { username, password }); if (result.status === 'success' && result.token) { const expiry = Date.now() + 7 * 24 * 60 * 60 * 1000; save(STORAGE.token, result.token); save(STORAGE.tokenExpiry, expiry); console.log('[聚影签到] 登录成功,token 已保存'); return result.token; } else { throw new Error(result.message || result.msg || '登录失败'); } } catch (e) { console.error('[聚影签到] 登录失败:', e.message); throw e; } } async function getToken() { const token = load(STORAGE.token); const expiry = load(STORAGE.tokenExpiry); if (token && expiry && Date.now() < expiry) { console.log('[聚影签到] 使用缓存的 token'); return token; } console.log('[聚影签到] Token 过期或不存在,重新登录'); const account = getAccount(); if (!account) throw new Error('未配置账号密码'); return await login(account.username, account.password); } async function getStats(token) { try { const result = await request('GET', API.stats, null, token); if (result.status === 'success') { return { totalDays: result.my_total_days || 0, checkedToday: result.checked_today || false, rewardPoints: result.reward_points || 0 }; } else { throw new Error(result.message || '获取状态失败'); } } catch (e) { console.error('[聚影签到] 获取状态失败:', e.message); throw e; } } async function doCheckin(token) { try { const result = await request('POST', API.checkin, null, token); if (result.status === 'success') { return { success: true, message: result.message || '签到成功', points: result.points || 5 }; } else { return { success: false, message: result.message || result.msg || '签到失败' }; } } catch (e) { if (e.message.includes('已签到')) { return { success: false, already: true, message: e.message }; } throw e; } } // ═══════════════════════════════════════════════════════════ // 主逻辑 // ═══════════════════════════════════════════════════════════ async function main() { console.log('[聚影签到] 开始执行签到任务...'); if (!isConfigured()) { notify('⚙️ 请配置聚影账号密码', '点击脚本菜单中的"配置账号"'); throw new Error('未配置账号密码'); } const account = getAccount(); console.log(`[聚影签到] 当前账号: ${maskAccount(account.username)}`); try { const token = await retryAsync(() => getToken()); const stats = await retryAsync(() => getStats(token)); console.log(`[聚影签到] 累计天数: ${stats.totalDays}, 今日已签: ${stats.checkedToday}`); if (stats.checkedToday) { notify('✓ 今日已签到', `累计签到 ${stats.totalDays} 天\n账号: ${maskAccount(account.username)}`); save(STORAGE.lastCheckin, new Date().toISOString()); return; } const result = await retryAsync(() => doCheckin(token)); if (result.success) { notify('✅ 聚影签到成功', `获得 ${result.points} 积分,累计 ${stats.totalDays + 1} 天\n账号: ${maskAccount(account.username)}`); save(STORAGE.lastCheckin, new Date().toISOString()); } else if (result.already) { notify('✓ 今日已签到', `累计签到 ${stats.totalDays} 天\n账号: ${maskAccount(account.username)}`); } else { notify('❌ 聚影签到失败', `${result.message}\n账号: ${maskAccount(account.username)}`); } } catch (e) { console.error('[聚影签到] 签到失败:', e); if (e.message.includes('登录') || e.message.includes('密码') || e.message.includes('未配置')) { save(STORAGE.token, null); save(STORAGE.tokenExpiry, null); } notify('❌ 聚影签到失败', `${e.message}\n账号: ${maskAccount(account.username)}`); throw e; } } // ═══════════════════════════════════════════════════════════ // 脚本菜单(仅页面加载时注册) // ═══════════════════════════════════════════════════════════ function registerMenus() { try { GM_registerMenuCommand('🔄 立即签到', main); GM_registerMenuCommand('⚙️ 配置账号', openConfigPanel); GM_registerMenuCommand('🗑️ 清除配置', clearConfig); if (isConfigured()) { const account = load(STORAGE.username); GM_registerMenuCommand(`📋 当前账号: ${maskAccount(account)}`, () => { alert(`当前配置账号: ${maskAccount(account)}\n\n点击"配置账号"可修改`); }); } } catch (e) { console.error('[聚影签到] 菜单注册失败:', e); } } // ═══════════════════════════════════════════════════════════ // 入口:返回 Promise(ScriptCat 后台脚本规范) // ═══════════════════════════════════════════════════════════ // 判断是否在后台环境 const isBackground = typeof window === 'undefined' || !window.location; if (!isBackground) { // 页面加载模式:注册菜单 + 显示配置面板 console.log('[聚影签到] 页面加载模式'); registerMenus(); // 延迟显示配置面板(等待页面加载完成) setTimeout(() => { showConfigPanelIfNeed(); }, 1000); } // ScriptCat 后台脚本:返回 Promise return new Promise((resolve, reject) => { if (isBackground) { // 后台定时任务:执行签到 console.log('[聚影签到] 后台定时任务模式'); main() .then(() => resolve('签到完成')) .catch(err => { console.error('[聚影签到] 执行失败:', err); reject(err); }); } else { // 页面加载模式:不阻塞,直接 resolve resolve('页面加载模式,菜单已注册'); } });