// ==UserScript== // @name 3G壁纸签到 // @namespace https://scriptcat.org/zh-CN/script-show-page/7352 // @version 0.0.1beta // @description 脚本猫后台定时脚本:自动登录、自动签到 3gbizhi.com。 // @author kfcsiko // @icon https://3gbizhi.com/favicon.ico // @background // 定时:每天 01:00–23:59 之间,自动运行一次,且“每天最多跑一次”(once)。 // 错过整天则次日再试;想固定 9:05 精确执行(无补签),改成:// @crontab 5 9 once * * // @crontab * 1-23 once * * // @connect www.3gbizhi.com // @connect qyapi.weixin.qq.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_log // ==/UserScript== /* ==UserConfig== Settings: username: title: 3G壁纸用户名 type: text default: "" password: title: 3G壁纸密码 type: text password: true default: "" EnableAutoLogin: title: 启用自动登录 description: 登录态失效时自动重新登录 type: checkbox default: true EnableDeduplication: title: 静默通知去重 description: 开启后,每种通知每天只发送一次 type: checkbox default: false Notice: title: 浏览器通知 description: 注意:后台脚本中浏览器通知(GM_notification)无法弹出,此设置仅对前台脚本生效 type: select default: 总是 values: [总是, 仅失败时, 首次签到时, 关闭] PushCondition: title: 企业微信推送条件 description: 企业微信机器人推送时机 type: select default: 关闭 values: [总是, 仅失败时, 首次签到时, 关闭] wework_key: title: 企业微信机器人Key description: 群机器人webhook地址中的key参数(如 838e6b16-xxxx),或直接粘贴完整webhook地址;填写后启用企业微信推送 type: text password: true default: "" ==/UserConfig== */ return (function () { 'use strict'; const BASE = 'https://www.3gbizhi.com'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36'; // 所有配置统一从 Settings 分组读取 const cfg = { username: () => GM_getValue('Settings.username', ''), password: () => GM_getValue('Settings.password', ''), autoLogin: () => GM_getValue('Settings.EnableAutoLogin', true), dedup: () => GM_getValue('Settings.EnableDeduplication', true), notice: () => GM_getValue('Settings.Notice', '总是'), pushCond: () => GM_getValue('Settings.PushCondition', '关闭'), wework: () => GM_getValue('Settings.wework_key', ''), }; const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; function xhRequest(method, url, opts) { opts = opts || {}; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: opts.headers || {}, data: opts.data !== undefined ? opts.data : null, timeout: 20000, onload: (res) => resolve(res), onerror: (err) => reject(new Error('网络错误:' + (err && err.error ? err.error : 'unknown'))), ontimeout: () => reject(new Error('请求超时')) }); }); } const txt = (res) => (res && res.responseText ? res.responseText : ''); async function getToken() { const res = await xhRequest('GET', BASE + '/user/login.html', { headers: { 'User-Agent': UA, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' } }); const html = txt(res); const m = html.match(/name="__token__"[^>]*value="([^"]*)"/) || html.match(/value="([^"]*)"[^>]*name="__token__"/); if (!m) throw new Error('登录页未找到 __token__(页面结构可能已变化)'); return m[1]; } async function login(token, account, password) { const body = `__token__=${encodeURIComponent(token)}&account=${encodeURIComponent(account)}&password=${encodeURIComponent(password)}`; const res = await xhRequest('POST', BASE + '/api/user/login', { headers: { 'User-Agent': UA, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Origin': BASE, 'Referer': BASE + '/user/login.html' }, data: body }); const t = txt(res); const cm = t.match(/"code"\s*:\s*(\d+)/); const mm = t.match(/"msg"\s*:\s*"([^"]*)"/); return { code: cm ? parseInt(cm[1], 10) : null, msg: mm ? mm[1] : '', raw: t }; } async function signin() { const res = await xhRequest('POST', BASE + '/api/user/setUserSignin', { headers: { 'User-Agent': UA, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Referer': BASE + '/user/index' }, data: '' }); const t = txt(res); const cm = t.match(/"code"\s*:\s*(\d+)/); const mm = t.match(/"msg"\s*:\s*"([^"]*)"/); return { code: cm ? parseInt(cm[1], 10) : null, msg: mm ? mm[1] : '', raw: t }; } async function getStatus() { try { const res = await xhRequest('GET', BASE + '/api/user/getUserSignin', { headers: { 'User-Agent': UA, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Referer': BASE + '/user/index' } }); const t = txt(res); const cont = t.match(/"continuous_days"\s*:\s*(\d+)/); const signed = t.match(/"signed_count"\s*:\s*(\d+)/); return { continuous: cont ? cont[1] : '?', signed: signed ? signed[1] : '?' }; } catch (e) { return null; } } function shouldSend(policy, category) { if (policy === '关闭') return false; if (policy === '总是') return true; if (policy === '仅失败时') return category === 'fail'; if (policy === '首次签到时') return category === 'success'; return false; } function buildWeComUrl(key) { if (!key) return null; if (/^https?:\/\//.test(key)) { try { const u = new URL(key); const k = u.searchParams.get('key'); return k ? `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${k}` : key; } catch (e) { return key; } } return `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; } // 企业微信 markdown 消息:加粗标题(##)、引用正文(>)、带时间戳 function buildWeComMarkdown(title, text) { const now = new Date().toLocaleString('zh-CN', { hour12: false }); const body = String(text).split('\n').map((l) => '> ' + l).join('\n'); return `## ${title}\n\n${body}\n> 🕒 ${now}`; } async function pushWeCom(title, text) { const key = cfg.wework(); if (!key) return; const url = buildWeComUrl(key); const content = buildWeComMarkdown(title, text); return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ msgtype: 'markdown', markdown: { content: content } }), onload: (res) => { try { const j = JSON.parse(res.responseText || '{}'); if (j.errcode && j.errcode !== 0) { GM_log(`[3gbizhi] 企微推送失败 errcode=${j.errcode} errmsg=${j.errmsg || ''}(检查 webhook key 是否正确、群机器人是否被停用)`); } else { GM_log(`[3gbizhi] 企微推送成功:${title}`); } } catch (e) { GM_log(`[3gbizhi] 企微响应解析失败:${res.responseText}`); } resolve(); }, onerror: (err) => { GM_log(`[3gbizhi] 企微推送网络错误:${err && err.error ? err.error : 'unknown'}`); resolve(); } }); }); } async function notify(category, title, text, bypassDedup) { const weworkKey = cfg.wework(); const pushCond = cfg.pushCond(); const wantBrowser = shouldSend(cfg.notice(), category); const wantWeCom = weworkKey && (category === 'fail' ? true : shouldSend(pushCond, category)); if (weworkKey && !wantWeCom) { GM_log(`[3gbizhi] 企微本次未推送:pushCond=${pushCond} category=${category}(如需推送请把“企业微信推送条件”设为“总是”或对应档)`); } if (!wantBrowser && !wantWeCom) return; if (cfg.dedup() && !bypassDedup) { const k = 'notif_' + category + '_' + todayStr(); if (await GM_getValue(k, false)) return; // 今天该类已发过,去重 await GM_setValue(k, true); } if (wantBrowser) { try { GM_notification({ title: title, text: String(text), timeout: 0 }); } catch (e) {} } if (wantWeCom) { await pushWeCom(title, text); } } function isSigned(sg) { return sg.code === 1 || /已签到|已经签到|今日已签|今天已签到|今天已经签到|请勿重复|重复签到|已签过|成功/.test(sg.msg || ''); } function isAlreadySigned(sg) { return /已签到|已经签到|今日已签|今天已签到|今天已经签到|请勿重复|重复签到|已签过/.test(sg.msg || ''); } function looksUnauthorized(sg) { return /请登录|未登录/.test(sg.msg || ''); } async function run(force) { const autoLogin = cfg.autoLogin(); let sg = await signin(); if (looksUnauthorized(sg)) { if (!autoLogin) { throw new Error('未登录:当前未启用自动登录,请先在浏览器登录 3gbizhi.com 或开启启用自动登录'); } const username = cfg.username(), password = cfg.password(); if (!username || !password) { throw new Error('未配置账号:网站会话已失效且需要自动登录,请在脚本猫「设置」面板填写用户名 / 密码'); } const token = await getToken(); const lg = await login(token, username, password); if (lg.code !== 1) throw new Error('登录失败:' + (lg.msg || '账号或密码错误')); sg = await signin(); // 重新签到 } if (isSigned(sg)) { const st = await getStatus(); const extra = st ? `连续${st.continuous}天 / 累计${st.signed}天` : ''; if (isAlreadySigned(sg)) { await notify('success', '今日已签到', '今天已经签到过了,无需重复。' + (extra ? '\n' + extra : ''), force); } else { await notify('success', '签到成功', (sg.msg || 'OK') + (extra ? '\n' + extra : ''), force); } } else { throw new Error('签到结果:' + (sg.msg || sg.raw || '未知返回')); } } if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('立即签到一次', () => { run(true).catch((e) => notify('fail', '签到异常', String(e.message || e), true)); }); GM_registerMenuCommand('清除今日通知去重', () => { GM_setValue('notif_success_' + todayStr(), false); GM_setValue('notif_fail_' + todayStr(), false); notify('success', '已清除', '下次运行将重新发送通知'); }); } return new Promise((resolve, reject) => { (async () => { try { await run(false); resolve('ok'); } catch (e) { const msg = String(e && e.message ? e.message : e); await notify('fail', '签到失败', msg, false); if (/网络错误|请求超时/.test(msg) && typeof CATRetryError !== 'undefined') { reject(new CATRetryError(msg, 30)); // 30 秒后重试 } else { reject(msg); } } })(); }); })();