// ==UserScript== // @name 微软积分 Token 助手(合并版) // @namespace local.rewards.bridge // @version 3.1.0 // @description 一键授权 + 结果反馈:后台可运行,任意页面有菜单,把 refresh_token 写入主脚本共享存储 // @author local // @crontab 0 0 1 1 * // @match *://*/* // @noframes // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_cookie // @grant GM_log // @connect login.live.com // @connect prod.rewardsplatform.microsoft.com // @storageName BingRewardsAuto_Shared // @run-at document-start // ==/UserScript== (function () { 'use strict'; const CLIENT_ID = '0000000040170455'; const REDIRECT_URI = 'https://login.live.com/oauth20_desktop.srf'; const SCOPE = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'; const TOKEN_URL = 'https://login.live.com/oauth20_token.srf'; const REWARDS_URL = 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613'; const AUTH_URL = 'https://login.live.com/oauth20_authorize.srf' + '?client_id=' + encodeURIComponent(CLIENT_ID) + '&response_type=code' + '&scope=' + encodeURIComponent(SCOPE) + '&redirect_uri=' + encodeURIComponent(REDIRECT_URI); // 主脚本(8491)读取的键,必须一字不差 const KEY_CODE = 'Config.code'; const KEY_TOKEN = 'Config.token'; const KEY_TIME = 'Config.tokenTime'; const IS_BG = (typeof document === 'undefined'); // ==================== 基础工具 ==================== function log(line) { try { GM_log('[Token助手] ' + line); } catch (e) {} if (!IS_BG) { try { console.log('[Token助手] ' + line); } catch (e) {} } } function notify(title, text) { log(title + ' | ' + text); try { GM_notification({ title: title, text: text, timeout: 20000 }); } catch (e) {} } function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } function extractCode(text) { if (!text) return ''; const m = String(text).match(/[?&]code=([^&\s#"'\\]+)/i); if (!m || !m[1]) return ''; let raw = m[1]; if (raw.indexOf('%') >= 0) { try { raw = decodeURIComponent(raw); } catch (e) {} } raw = raw.trim(); return raw.length > 20 ? raw : ''; } function request(opts) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest(Object.assign({ timeout: 30000, onload: function (r) { resolve(r); }, onerror: function () { reject(new Error('网络请求失败(跨域或站点访问权限未授权)')); }, ontimeout: function () { reject(new Error('请求超时')); } }, opts)); }); } function postForm(url, params) { const body = Object.keys(params) .map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }) .join('&'); return request({ method: 'POST', url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: body }); } async function fetchPoints(accessToken) { try { const r = await request({ method: 'GET', url: REWARDS_URL, headers: { 'Authorization': 'Bearer ' + accessToken }, timeout: 20000 }); const j = JSON.parse(r.responseText); const resp = j.response || j; return resp.balance != null ? resp.balance : null; } catch (e) { return null; } } // ---------- 读取微软登录 Cookie(判断浏览器登录态)---------- function listCookies(domain) { return new Promise(function (resolve) { if (typeof GM_cookie === 'undefined' || !GM_cookie || !GM_cookie.list) { resolve(null); return; } let done = false; const finish = function (v) { if (!done) { done = true; resolve(v); } }; try { const ret = GM_cookie.list({ domain: domain }, function (cookies, err) { finish(err ? [] : (cookies || [])); }); if (ret && typeof ret.then === 'function') { ret.then(function (c) { finish(c || []); }).catch(function () { finish([]); }); } } catch (e) { finish([]); return; } setTimeout(function () { finish([]); }, 3000); }); } const AUTH_COOKIE_HINTS = ['mspauth', 'mspauthp', '__host-msaauth', 'rpssecauth', 'msaoffline', 'wlid', 'mscc', 'mspconsent']; async function detectLogin() { let list = await listCookies('.live.com'); if (list === null) return { available: false, loggedIn: false, hit: [] }; if (!list.length) { const l2 = await listCookies('.login.live.com'); if (l2 && l2.length) list = l2; } const hit = []; list.forEach(function (c) { const n = String(c.name || '').toLowerCase(); AUTH_COOKIE_HINTS.forEach(function (h) { if (n === h || n.indexOf(h) === 0) hit.push(c.name); }); }); const uniq = hit.filter(function (v, i) { return hit.indexOf(v) === i; }); return { available: true, loggedIn: uniq.length > 0, hit: uniq, total: list.length }; } async function exchangeCode(code) { let r; try { r = await postForm(TOKEN_URL, { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, grant_type: 'authorization_code', code: code, scope: SCOPE }); } catch (e) { return { ok: false, reason: e.message }; } let d = null; try { d = JSON.parse(r.responseText); } catch (e) { return { ok: false, reason: 'HTTP ' + r.status + ',返回不是 JSON' }; } if (r.status === 200 && d && d.refresh_token) { return { ok: true, refreshToken: d.refresh_token, accessToken: d.access_token }; } return { ok: false, reason: (d && d.error) ? d.error + ' - ' + (d.error_description || '') : ('HTTP ' + r.status) }; } async function renewToken(refreshToken) { let r; try { r = await postForm(TOKEN_URL, { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, grant_type: 'refresh_token', refresh_token: refreshToken, scope: SCOPE }); } catch (e) { return { ok: false, reason: e.message }; } let d = null; try { d = JSON.parse(r.responseText); } catch (e) { return { ok: false, reason: 'HTTP ' + r.status + ',返回不是 JSON' }; } if (d && d.access_token) { return { ok: true, accessToken: d.access_token, refreshToken: d.refresh_token || refreshToken }; } return { ok: false, reason: (d && d.error) ? d.error + ' - ' + (d.error_description || '') : ('HTTP ' + r.status) }; } async function applyToken(refreshToken, accessToken, source) { refreshToken = String(refreshToken || '').trim(); if (refreshToken.length < 100) { notify('❌ 写入失败', 'refresh_token 长度只有 ' + refreshToken.length + ',明显不对'); return false; } GM_setValue(KEY_TOKEN, refreshToken); GM_setValue(KEY_TIME, Date.now()); GM_setValue(KEY_CODE, ''); log('已写入 Config.token(长度 ' + refreshToken.length + ',来源:' + source + ')'); return true; } function openAuthPage() { try { if (typeof GM_openInTab === 'function') { GM_openInTab(AUTH_URL, { active: true }); return true; } } catch (e) { log('GM_openInTab 失败:' + e.message); } if (!IS_BG) { try { return !!window.open(AUTH_URL, '_blank'); } catch (e) {} } return false; } // ==================== 后台模式 ==================== async function runBackground() { log('后台运行开始'); const saved = GM_getValue(KEY_TOKEN, false); if (saved && String(saved).length > 100) { const r = await renewToken(saved); if (r.ok) { await applyToken(r.refreshToken, r.accessToken, '续期验证'); const p = await fetchPoints(r.accessToken); notify('✅ Token 正常', '续期成功' + (p != null ? ',当前积分 ' + p : '')); return; } log('续期失败:' + r.reason); } const t0 = Date.now(); openAuthPage(); notify('🔑 请登录微软账号', '登录后会自动完成,结果会用通知告诉你'); for (let i = 0; i < 100; i++) { await sleep(3000); const tok = GM_getValue(KEY_TOKEN, false); const ts = GM_getValue(KEY_TIME, 0); if (tok && ts >= t0) { const r = await renewToken(tok); const p = r.ok ? await fetchPoints(r.accessToken) : null; notify('✅ 授权成功', 'Token 已更新' + (p != null ? ',当前积分 ' + p : '')); return; } } notify('⚠️ 未检测到授权完成', '请确认已在打开的页面登录微软账号'); } // ==================== 页面模式 ==================== /** 唯一的主力按钮:打开授权页 → 轮询结果 → 通知结论 */ async function actionOneClickAuth() { const saved = GM_getValue(KEY_TOKEN, false); if (saved && String(saved).length > 100) { const r = await renewToken(saved); if (r.ok) { const p = await fetchPoints(r.accessToken); notify('✅ 当前 Token 有效', '不需要重新授权' + (p != null ? ',当前积分 ' + p : '')); return; } log('现有 Token 已失效:' + r.reason); } const t0 = Date.now(); const opened = openAuthPage(); notify('🔑 已打开授权页', (opened ? '请在该页面登录微软账号。' : '请手动访问:' + AUTH_URL) + '完成后会自动通知你结果'); for (let i = 0; i < 50; i++) { // 最多等 2.5 分钟 await sleep(3000); const tok = GM_getValue(KEY_TOKEN, false); const ts = GM_getValue(KEY_TIME, 0); if (tok && ts >= t0) { const r = await renewToken(tok); const p = r.ok ? await fetchPoints(r.accessToken) : null; notify('✅ 授权成功', 'Token 已写入主脚本' + (p != null ? ',当前积分 ' + p : '')); return; } } notify('⚠️ 授权结果未知', '2.5 分钟内没有检测到新 Token。如果刚才确实登录了,等主脚本下一轮跑完再点「🔍 检查 Token」'); } /** 登录态检查:读微软登录 Cookie + 汇总 Token 状态,未登录时给一个「去登录」按钮 */ async function actionLoginState() { notify('👤 正在检查登录态', '读取微软登录 Cookie 与 Token 状态…'); const st = await detectLogin(); const tok = GM_getValue(KEY_TOKEN, false); let tokenOk = false; let points = null; if (tok && String(tok).length > 100) { const r = await renewToken(tok); if (r.ok) { tokenOk = true; points = await fetchPoints(r.accessToken); } } const loginLine = st.available ? (st.loggedIn ? '已登录 ✅ 凭据:' + st.hit.join('、') : '未登录 ❌ 没找到登录 Cookie') : '无法读取(GM_cookie 未授权,可在脚本设置里放开)'; const tokenLine = tokenOk ? '有效 ✅' + (points != null ? ' 当前积分 ' + points : '') : (tok ? '已失效 ❌' : '未获取 ❌'); const head = '浏览器登录态:' + loginLine + '\nToken:' + tokenLine; if (tokenOk) { alert(head + '\n\n一切正常,不需要做任何操作。'); return; } if (st.loggedIn) { if (confirm(head + '\n\n浏览器已登录,但还没有可用的 Token。\n\n点「确定」立即授权(自动完成,无需复制):')) { actionOneClickAuth(); } return; } if (confirm(head + '\n\n没有检测到微软登录态。\n\n点「确定」前往登录页面:')) { openAuthPage(); } } /** 只检查已保存的 Token 还能不能用(续期测试,最可靠) */ async function actionCheckToken() { const tok = GM_getValue(KEY_TOKEN, false); if (!tok || String(tok).length < 100) { notify('❌ 还没有 Token', '请先点「🔑 一键授权」'); return; } notify('🔎 正在检查 Token', '正在用 refresh_token 做一次续期测试…'); const r = await renewToken(tok); if (!r.ok) { notify('❌ Token 已失效', r.reason + ' — 请点「🔑 一键授权」'); return; } const p = await fetchPoints(r.accessToken); notify('✅ Token 正常', (p != null ? '当前积分 ' + p : '续期成功') + ';写入时间 ' + new Date(GM_getValue(KEY_TIME, 0)).toLocaleString()); } function runPage() { // 授权跳转页:抢在主脚本关页之前捕获并换取 if (location.hostname === 'login.live.com' && location.pathname === '/oauth20_desktop.srf') { const code = extractCode(location.href); if (!code) return; // 主脚本会在这个页面 200ms 后 window.close(),把关闭掐掉,保证换 Token 能完成 try { window.close = function () { log('已阻止自动关页'); }; } catch (e) {} exchangeCode(code).then(function (res) { if (res.ok) { return applyToken(res.refreshToken, res.accessToken, '授权页自动换取').then(function () { const t = GM_getValue(KEY_TIME, 0); notify('✅ 授权成功', 'Token 已写入,可在当前页关闭标签;' + new Date(t).toLocaleString()); return null; }); } notify('❌ 自动换取失败', res.reason); return null; }); return; } // 普通页面:两个按钮 const MENU_ITEMS = [ ['🔑 一键授权', actionOneClickAuth], ['👤 登录态检查', actionLoginState], ['🔍 检查 Token', actionCheckToken] ]; let menuIndex = 0; let menuTries = 0; let panelMounted = false; function mountPanel(reason) { if (panelMounted) return; panelMounted = true; log('改用悬浮面板:' + reason); const build = function () { if (document.getElementById('__rewards_bridge_panel')) return; const box = document.createElement('div'); box.id = '__rewards_bridge_panel'; box.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'z-index:2147483647', 'background:#1f2430', 'color:#fff', 'border-radius:10px', 'padding:8px 10px', 'font:12px/1.6 system-ui,"Microsoft YaHei",sans-serif', 'box-shadow:0 6px 20px rgba(0,0,0,.35)', 'opacity:.94', 'display:flex', 'gap:10px', 'align-items:center', 'user-select:none' ].join(';'); const mk = function (label, fn) { const b = document.createElement('span'); b.textContent = label; b.style.cssText = 'cursor:pointer;padding:2px 8px;border-radius:6px;background:#2f3746;white-space:nowrap'; b.addEventListener('mouseenter', function () { b.style.background = '#3d4859'; }); b.addEventListener('mouseleave', function () { b.style.background = '#2f3746'; }); b.addEventListener('click', function (ev) { ev.stopPropagation(); try { fn(); } catch (e) { alert('出错:' + e.message); } }); return b; }; box.appendChild(mk('🔑 一键授权', actionOneClickAuth)); box.appendChild(mk('👤 登录态', actionLoginState)); box.appendChild(mk('🔍 检查', actionCheckToken)); const close = document.createElement('span'); close.textContent = '×'; close.style.cssText = 'cursor:pointer;margin-left:4px;opacity:.6;padding:0 4px'; close.addEventListener('click', function (ev) { ev.stopPropagation(); box.remove(); }); box.appendChild(close); (document.body || document.documentElement).appendChild(box); }; if (document.body) { build(); } else { document.addEventListener('DOMContentLoaded', build, { once: true }); } } function pumpMenu() { if (menuIndex >= MENU_ITEMS.length) return; if (typeof GM_registerMenuCommand !== 'function') { mountPanel('菜单 API 不可用'); return; } let ret = -1; try { ret = GM_registerMenuCommand(MENU_ITEMS[menuIndex][0], MENU_ITEMS[menuIndex][1]); } catch (e) { ret = -1; } if (ret === -1 || ret === undefined) { menuTries++; if (menuTries < 15) { setTimeout(pumpMenu, 200); } else { mountPanel('菜单注册超时'); } return; } menuIndex++; menuTries = 0; pumpMenu(); } setTimeout(pumpMenu, 0); } if (IS_BG) { runBackground().catch(function (e) { notify('❌ 后台运行出错', e && e.message ? e.message : String(e)); }); } else { runPage(); } })();