// ==UserScript== // @name 微软积分 Token 后台助手 // @namespace local.rewards.bridge.bg // @version 1.0.0 // @description 后台脚本:一键授权 / 续期,把 refresh_token 写入主脚本共享存储。不需要打开任何网页。 // @author local // @background // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_openInTab // @grant GM_log // @connect login.live.com // @connect prod.rewardsplatform.microsoft.com // @storageName BingRewardsAuto_Shared // ==/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'; function log(line) { try { GM_log('[后台助手] ' + line); } catch (e) {} } function notify(title, text) { log(title + ' | ' + text); try { GM_notification({ title: title, text: text, timeout: 10000 }); } catch (e) {} } function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } function extractCode(text) { if (!text) return ''; const s = String(text); const m = s.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 > 50 ? 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 refreshToken(rt) { let r; try { r = await postForm(TOKEN_URL, { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, grant_type: 'refresh_token', refresh_token: rt, 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 || rt }; } return { ok: false, reason: (d && d.error) ? d.error + ' - ' + (d.error_description || '') : ('HTTP ' + r.status) }; } 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 getPoints(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 { points: resp.balance, profile: resp.profile }; } catch (e) { return null; } } /** * 用当前浏览器的登录态直接请求授权地址,跟随重定向后从 finalUrl 里取 code。 * 已经登录时,这一次 GET 本身就会签发一个全新的、未被别人用过的授权码。 */ async function fetchCodeViaHttp() { const r = await request({ method: 'GET', url: AUTH_URL, timeout: 30000 }); const candidates = [r.finalUrl, r.responseText]; for (let i = 0; i < candidates.length; i++) { const code = extractCode(candidates[i]); if (code) return code; } return ''; } async function applyToken(refreshToken, accessToken, source) { GM_setValue(KEY_TOKEN, refreshToken); GM_setValue(KEY_TIME, Date.now()); GM_setValue(KEY_CODE, ''); log('已写入 Config.token(长度 ' + String(refreshToken).length + ',来源:' + source + ')'); let extra = ''; const p = accessToken ? await getPoints(accessToken) : null; if (p && p.points != null) extra = ',当前积分 ' + p.points; notify('✅ Token 已写入', '来源:' + source + extra); } // ================= 主流程 ================= async function main() { log('开始运行'); // 1) 已有 token:先验证还能不能续期 const saved = GM_getValue(KEY_TOKEN, false); if (saved && String(saved).length > 100) { log('已有 token(长度 ' + String(saved).length + '),验证续期'); const r = await refreshToken(saved); if (r.ok) { await applyToken(r.refreshToken, r.accessToken, '续期验证'); return; } log('续期失败:' + r.reason); } // 2) 没有可用 token:直接用登录态取码 notify('🔑 开始授权', '正在尝试自动获取,无需操作'); let code = ''; try { code = await fetchCodeViaHttp(); } catch (e) { log('自动取码异常:' + e.message); } if (code) { log('自动拿到授权码(长度 ' + code.length + '),立即换取'); const res = await exchangeCode(code); if (res.ok) { await applyToken(res.refreshToken, res.accessToken, '后台自动换取'); return; } log('换取失败:' + res.reason + '(继续走人工登录)'); } else { log('自动取码失败(多为未登录),转人工登录'); } // 3) 兜底:打开授权页,轮询等待登录完成 try { GM_openInTab(AUTH_URL, { active: true }); notify('🔑 请登录微软账号', '登录后会自动完成,不需要复制任何内容'); } catch (e) { notify('⚠️ 需要登录', '请手动打开:' + AUTH_URL); } for (let i = 0; i < 40; i++) { // 最多等 2 分钟 await sleep(3000); let c = ''; try { c = await fetchCodeViaHttp(); } catch (e) { continue; } if (!c) continue; log('轮询拿到授权码,立即换取'); const res = await exchangeCode(c); if (res.ok) { await applyToken(res.refreshToken, res.accessToken, '登录后自动换取'); return; } log('轮询换取失败:' + res.reason); } notify('❌ 未能完成授权', '可能没有登录成功,请重试一次'); } main().catch(function (e) { notify('❌ 运行出错', e && e.message ? e.message : String(e)); }); })();