// ==UserScript== // @name WorkBuddy 加油站领积分 // @namespace https://scriptcat.org/zh-CN/search?script_type=3&sort=createtime // @version 1.0.0 // @description WorkBuddy「Buddy加油站」每日自动领积分,需保持登录状态 // @author kfcsiko // @crontab * 1-23 once * * // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_log // @grant GM_getValue // @grant GM_setValue // @connect www.workbuddy.cn // @connect qyapi.weixin.qq.com // @connect api.chuckfang.com // @icon https://s41.ax1x.com/2026/09/21/pnQulfs.png // ==/UserScript== /* ==UserConfig== Config: Notice: title: 浏览器通知 type: select default: 总是 values: [总是, 仅失败时, 关闭] PushCondition: title: 推送条件 type: select default: 关闭 values: [总是, 仅失败时, 关闭] meow_id: title: meow用户ID description: 在 https://api.chuckfang.com/ 中使用的用户ID,填写后自动启用喵推送 type: text password: true default: "" wework_key: title: 企业微信机器人Key description: 群机器人的 webhook key,填写后自动启用企业微信推送 type: text password: true default: "" ==/UserConfig== */ var ORIGIN = "https://www.workbuddy.cn"; var STATUS_PATHS = [ "/v2/billing/meter/checkin-activity-status", "/billing/meter/checkin-activity-status", "/v2/billing/meter/checkin-status", "/billing/meter/checkin-status", ]; var CLAIM_PATHS = [ "/v2/billing/meter/daily-checkin", "/billing/meter/daily-checkin", ]; function log(msg, level) { var text = "[WorkBuddy签到] " + msg; try { GM_log(text, level || "info"); } catch (e) { console.log(text); } } function getConfig(key, defaultValue, validValues = null) { let val = GM_getValue(`Config.${key}`, defaultValue); if (Array.isArray(val)) { val = val.length > 0 ? val[0] : defaultValue; } if (typeof val !== "string") { val = String(val); } if (validValues && !validValues.includes(val)) { GM_log(`配置项 ${key} 值“${val}”无效,回退至默认值“${defaultValue}”`); val = defaultValue; } return val; } function pushToMeow(title, content, imgUrl = "https://s41.ax1x.com/2026/09/21/pnQulfs.png") { return new Promise((resolve) => { const meowId = getConfig("meow_id", ""); if (!meowId) { resolve(false); return; } const apiUrl = `https://api.chuckfang.com/${encodeURIComponent(meowId)}`; GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ title, msg: content, imgUrl }), timeout: 15000, onload: function(resp) { try { const json = JSON.parse(resp.responseText); if (resp.status >= 200 && resp.status < 300 && json.status === 200) { GM_log(`喵推送成功: ${title}`); resolve(true); } else { GM_log(`喵推送失败: ${resp.status} ${resp.responseText}`); resolve(false); } } catch (e) { GM_log(`喵推送解析错误: ${e}`); resolve(false); } }, onerror: (err) => { GM_log(`喵推送错误: ${err}`); resolve(false); }, ontimeout: () => { GM_log(`喵推送超时`); resolve(false); } }); }); } function pushToWeWork(title, content) { return new Promise((resolve) => { const key = getConfig("wework_key", ""); if (!key) { resolve(false); return; } const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ msgtype: "text", text: { content: `${title}\n${content}`.trim() } }), timeout: 15000, onload: function(resp) { try { const json = JSON.parse(resp.responseText); if (json.errcode === 0) { GM_log(`企业微信推送成功: ${title}`); resolve(true); } else { GM_log(`企业微信推送失败: ${json.errmsg}`); resolve(false); } } catch (e) { GM_log(`企业微信响应解析错误: ${e}`); resolve(false); } }, onerror: (err) => { GM_log(`企业微信错误: ${err}`); resolve(false); }, ontimeout: () => { GM_log(`企业微信超时`); resolve(false); } }); }); } async function notifyWithPush(title, text, isError = false) { const notice = getConfig("Notice", "总是", ["总是", "仅失败时", "关闭"]); const pushCondition = getConfig("PushCondition", "关闭", ["总是", "仅失败时", "关闭"]); if (notice === "总是" || (notice === "仅失败时" && isError)) { try { GM_notification({ title: title, text: text, timeout: 8000, error: isError }); } catch (e) { /* 通知失败不影响主流程 */ } } let shouldPush = false; if (pushCondition === "总是") shouldPush = true; else if (pushCondition === "仅失败时" && isError) shouldPush = true; else shouldPush = false; if (shouldPush) { await Promise.allSettled([ pushToMeow(title, text, "https://s41.ax1x.com/2026/09/21/pnQulfs.png"), pushToWeWork(title, text) ]); } } async function notify(title, text, isError) { await notifyWithPush(title, text, !!isError); } var NOTIFY_DEDUP_MS = 60 * 60 * 1000; async function notifyOnce(key, title, text, isError) { var now = Date.now(); try { var last = num(GM_getValue("lastNotifyAt." + key, 0)); if (last && now - last < NOTIFY_DEDUP_MS) { log("(通知去重) " + key + " 距上次通知不足 60 分钟,跳过本次通知/推送", "warn"); return; } GM_setValue("lastNotifyAt." + key, now); } catch (e) { /* 存储异常不阻断通知,宁可多发不可漏发 */ } await notify(title, text, isError); } function post(path, timeout) { return new Promise(function (resolve) { GM_xmlhttpRequest({ method: "POST", url: ORIGIN + path, headers: { "Content-Type": "application/json", Accept: "application/json", }, data: "{}", timeout: timeout || 20000, anonymous: false, onload: function (res) { var body = null; try { body = JSON.parse(res.responseText); } catch (e) { body = null; } resolve({ path: path, status: res.status, body: body, raw: (res.responseText || "").slice(0, 240), }); }, ontimeout: function () { resolve({ path: path, status: 0, error: "timeout" }); }, onerror: function (err) { resolve({ path: path, status: 0, error: String(err || "network error") }); }, }); }); } function pick(o, keys) { for (var i = 0; i < keys.length; i++) { if (o && o[keys[i]] !== undefined && o[keys[i]] !== null) return o[keys[i]]; } return undefined; } function num(v) { return (typeof v === "number" && isFinite(v)) ? v : 0; } function readStatus(req) { var b = req.body || {}; var d = (b.data && typeof b.data === "object") ? b.data : {}; var src = Object.keys(d).length ? d : b; return { code: b.code, msg: b.msg || "", active: pick(src, ["active"]), todayCheckedIn: !!(pick(src, ["today_checked_in", "todayCheckedIn"])), streakDays: num(pick(src, ["streak_days", "streakDays"])), dailyCredit: num(pick(src, ["daily_credit", "dailyCredit"])), todayCredit: num(pick(src, ["today_credit", "todayCredit"])), nextStreakDay: num(pick(src, ["next_streak_day", "nextStreakDay"])), streakBonusDays: num(pick(src, ["streak_bonus_days", "streakBonusDays"])), streakBonusCredit: num(pick(src, ["streak_bonus_credit", "streakBonusCredit"])), periodTag: pick(src, ["period_tag", "periodTag"]) || "", }; } function readClaim(req) { var b = req.body || {}; var d = (b.data && typeof b.data === "object") ? b.data : {}; var src = Object.keys(d).length ? d : b; return { code: b.code, msg: b.msg || "", credit: num(pick(src, ["credit"])), streakDays: num(pick(src, ["streak_days", "streakDays"])), isStreakDay: !!(pick(src, ["is_streak_day", "isStreakDay"])), nextStreakDay: num(pick(src, ["next_streak_day", "nextStreakDay"])), }; } function isAlreadyText(s) { return /已签到|已经签到|已领取|重复|请明天|already/i.test(String(s || "")); } function remember(entry) { try { var list = GM_getValue("history", []); var arr = Array.isArray(list) ? list : []; arr.unshift(entry); GM_setValue("history", arr.slice(0, 30)); GM_setValue("lastResult", entry); } catch (e) { /* 忽略存储异常 */ } } function brief(r) { if (!r) return "-"; if (r.status === 0) return r.path + " -> 网络失败(" + (r.error || "?") + ")"; var code = (r.body && typeof r.body.code !== "undefined") ? r.body.code : "?"; var hasData = !!(r.body && r.body.data && typeof r.body.data === "object"); var active = hasData ? r.body.data.active : undefined; return r.path + " -> HTTP" + r.status + " code=" + code + (hasData ? " data有 active=" + active : " 无data") + (r.body && r.body.msg ? " msg=" + r.body.msg : ""); } // ===== 主流程(后台脚本必须手动返回 Promise,脚本管理器才能监控) ===== return new Promise(function (resolve, reject) { (async function main() { log("v1.0.0 开始执行,目标站点 " + ORIGIN); var probes = []; var st = null, stReq = null, netErr = null; for (var i = 0; i < STATUS_PATHS.length; i++) { var r = await post(STATUS_PATHS[i]); probes.push(brief(r)); if (r.status === 401 || r.status === 403) { var uma = "登录态已失效(HTTP " + r.status + ")。请用浏览器打开 " + ORIGIN + " 重新登录后再试。"; log(uma, "error"); log("探测过程:" + probes.join(" | "), "warn"); await notifyOnce("unauthorized", "WorkBuddy 加油站:需要重新登录", uma, true); remember({ at: new Date().toISOString(), ok: false, reason: "unauthorized", httpStatus: r.status }); return resolve("unauthorized"); } if (r.status === 0) { netErr = r.error || "network error"; continue; } if (r.body && typeof r.body.code === "number" && r.body.code === 0) { st = readStatus(r); stReq = r; break; } } log("状态探测:" + probes.join(" | "), "info"); if (!st) { var why = netErr ? ("网络失败:" + netErr) : ("所有候选状态路径均未返回可用数据。探测过程:" + probes.join(" | ")); log(why, "error"); remember({ at: new Date().toISOString(), ok: false, reason: "status-failed", detail: why, probes: probes }); var err = (typeof CATRetryError === "function") ? new CATRetryError("WorkBuddy 加油站:状态查询失败 - " + why, 600) : new Error(why); return reject(err); } log("状态命中 " + stReq.path + " -> today_checked_in=" + st.todayCheckedIn + " streak_days=" + st.streakDays + " active=" + st.active + " daily_credit=" + st.dailyCredit + " today_credit=" + st.todayCredit + (st.periodTag ? " period=" + st.periodTag : "")); if (st.todayCheckedIn) { var okMsg = "今日已领取,无需重复领取。连续 " + st.streakDays + " 天" + (st.streakBonusCredit ? ",连续奖励 " + st.streakBonusCredit : "") + "。"; log(okMsg); await notify("WorkBuddy 加油站", okMsg, false); remember({ at: new Date().toISOString(), ok: true, reason: "already-checked-in", streakDays: st.streakDays, statusPath: stReq.path, probes: probes }); return resolve("already"); } if (st.active === false) { log("注意:状态接口返回 active=false(该字段在客户端仅控制菜单入口展示)。" + "仍按计划尝试领取一次,以服务端返回为准。", "warn"); } var claimResp = null; var claimProbes = []; for (var j = 0; j < CLAIM_PATHS.length; j++) { var cr = await post(CLAIM_PATHS[j]); claimProbes.push(brief(cr)); if (cr.status === 401 || cr.status === 403) { var umc = "领取时登录态失效(HTTP " + cr.status + ")。请重新登录 " + ORIGIN + " 后再试。"; log(umc, "error"); await notifyOnce("unauthorized", "WorkBuddy 加油站:需要重新登录", umc, true); remember({ at: new Date().toISOString(), ok: false, reason: "unauthorized-on-claim" }); return resolve("unauthorized"); } if (cr.status === 0) continue; if (cr.status === 404) continue; if (cr.body && typeof cr.body.code === "number") { claimResp = cr; if (cr.body.code === 0 || isAlreadyText(cr.body.msg)) break; break; } claimResp = cr; break; } log("领取探测:" + claimProbes.join(" | "), "info"); if (!claimResp) { var w2 = "所有候选领取路径均无法连通。探测:" + claimProbes.join(" | "); log(w2, "error"); remember({ at: new Date().toISOString(), ok: false, reason: "claim-network", detail: w2 }); var e2 = (typeof CATRetryError === "function") ? new CATRetryError(w2, 600) : new Error(w2); return reject(e2); } var claim = readClaim(claimResp); if (claim.code === 0) { var msg = "领取成功,+" + (claim.credit || st.dailyCredit || st.todayCredit || 0) + " 积分" + (claim.streakDays ? ",连续 " + claim.streakDays + " 天" : "") + (claim.isStreakDay ? "(连续领取奖励日)" : ""); log(msg); await notify("WorkBuddy 加油站", msg, false); remember({ at: new Date().toISOString(), ok: true, reason: "claimed", credit: claim.credit, streakDays: claim.streakDays, statusPath: stReq.path, claimPath: claimResp.path, probes: probes, claimProbes: claimProbes }); return resolve("claimed"); } if (isAlreadyText(claim.msg)) { var am = "服务端提示已签到(code=" + claim.code + "):" + claim.msg; log(am, "warn"); remember({ at: new Date().toISOString(), ok: true, reason: "already-by-server", detail: am }); return resolve("already"); } var why3 = "领取失败:code=" + claim.code + " msg=" + (claim.msg || "") + " HTTP " + claimResp.status + " path=" + claimResp.path + " raw=" + (claimResp.raw || ""); log(why3, "error"); await notifyOnce("claim-failed", "WorkBuddy 加油站", why3, true); remember({ at: new Date().toISOString(), ok: false, reason: "claim-failed", detail: why3, probes: probes, claimProbes: claimProbes }); return resolve("failed"); })().then(resolve).catch(function (e) { log("未捕获异常:" + (e && e.message ? e.message : String(e)), "error"); reject(e); }); });