diff --git a/.github/workflows/AutoLabelIssue.yml b/.github/workflows/AutoLabelIssue.yml index 58ccb049..2c4bace8 100644 --- a/.github/workflows/AutoLabelIssue.yml +++ b/.github/workflows/AutoLabelIssue.yml @@ -14,7 +14,7 @@ jobs: fetch-depth: 0 - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/AutoLablePR.yml b/.github/workflows/AutoLablePR.yml index cb6a4abf..1c5f05a0 100644 --- a/.github/workflows/AutoLablePR.yml +++ b/.github/workflows/AutoLablePR.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/Daily.yml b/.github/workflows/Daily.yml index a4695afe..da5218d3 100644 --- a/.github/workflows/Daily.yml +++ b/.github/workflows/Daily.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/Prerelease.yml b/.github/workflows/Prerelease.yml index 387437f0..ec92742c 100644 --- a/.github/workflows/Prerelease.yml +++ b/.github/workflows/Prerelease.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index e9c59139..8dedca9a 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/UpdateVersion.yml b/.github/workflows/UpdateVersion.yml index 6112841f..e43ba0c8 100644 --- a/.github/workflows/UpdateVersion.yml +++ b/.github/workflows/UpdateVersion.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Generate a token id: generate_token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/Update.json b/Update.json index 8fb7414c..3d3ee4cb 100644 --- a/Update.json +++ b/Update.json @@ -3519,6 +3519,103 @@ } ], "Notes": "No release notes were provided for this release." + }, + "3.4.1": { + "UpdateDate": 1774675280384, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 961, + "Description": "feat: Cross-device settings sync via cloud backend" + } + ], + "Notes": "No release notes were provided for this release." + }, + "3.4.2": { + "UpdateDate": 1774706085890, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 963, + "Description": "feat: Add hourly periodic cloud settings sync" + } + ], + "Notes": "Add hourly automatic cloud settings sync. Every hour, if CloudSync is enabled, settings are downloaded from the cloud and applied locally, then local settings are uploaded to the cloud. This keeps settings in sync across devices without requiring a visit to the settings page." + }, + "3.4.3": { + "UpdateDate": 1775972372292, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 969, + "Description": "Migrate XMOJ-BBS client endpoints to /v1 routes" + } + ], + "Notes": "No release notes were provided for this release." + }, + "3.4.4": { + "UpdateDate": 1775993504358, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 975, + "Description": "Revert /v1 endpoint migration" + } + ], + "Notes": "Revert the `/v1` endpoint migration from #969 to restore original API routes." + }, + "3.4.5": { + "UpdateDate": 1776009434318, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 973, + "Description": "Fix loginpage.php Loop" + } + ], + "Notes": "Fix loginpage.php loop." + }, + "3.4.6": { + "UpdateDate": 1777708421566, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 980, + "Description": "Switch all backend endpoints to api.xmoj-script.uk" + } + ], + "Notes": "No release notes were provided for this release." + }, + "3.5.0": { + "UpdateDate": 1778930758179, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 961, + "Description": "feat: Cross-device settings sync via cloud backend" + }, + { + "PR": 963, + "Description": "feat: Add hourly periodic cloud settings sync" + }, + { + "PR": 969, + "Description": "Migrate XMOJ-BBS client endpoints to /v1 routes" + }, + { + "PR": 975, + "Description": "Revert /v1 endpoint migration" + }, + { + "PR": 973, + "Description": "Fix loginpage.php Loop" + }, + { + "PR": 980, + "Description": "Switch all backend endpoints to api.xmoj-script.uk" + } + ], + "Notes": "No release notes were provided for this release." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 19f60be8..f4c9e6bf 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.4.0 +// @version 3.5.0 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -21,10 +21,11 @@ // @grant GM_setValue // @grant GM_getValue // @grant GM_cookie -// @homepage https://www.xmoj-bbs.me/ -// @supportURL https://support.xmoj-bbs.me/form/8050213e-c806-4680-b414-0d1c48263677 +// @homepage https://www.xmoj-script.uk/ +// @supportURL https://github.com/XMOJ-Script-dev/XMOJ-Script/issues // @connect api.xmoj-bbs.tech // @connect api.xmoj-bbs.me +// @connect api.xmoj-script.uk // @connect challenges.cloudflare.com // @connect cppinsights.io // @connect cdnjs.cloudflare.com @@ -505,7 +506,7 @@ let RequestAPI = (Action, Data, CallBack) => { } GM_xmlhttpRequest({ method: "POST", - url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-bbs.me/") + Action, + url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-script.uk/") + Action, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache", @@ -532,6 +533,58 @@ let RequestAPI = (Action, Data, CallBack) => { } } }; +let SyncSettingsToCloud = (CallBack) => { + if (!CurrentUsername) { + if (CallBack) CallBack({ Success: false, Message: "用户未登录" }); + return; + } + if (!UtilityEnabled("CloudSync")) { + if (CallBack) CallBack({ Success: false, Message: "云同步已禁用" }); + return; + } + let Settings = {}; + for (let i = 0; i < localStorage.length; i++) { + let key = localStorage.key(i); + if (key && key.startsWith("UserScript-Setting-")) { + Settings[key.replace("UserScript-Setting-", "")] = localStorage.getItem(key); + } + } + RequestAPI("SetUserSettings", {"Settings": JSON.stringify(Settings)}, (Response) => { + if (UtilityEnabled("DebugMode")) { + if (Response.Success) { + console.log("设置已同步到云端"); + } else { + console.error("设置云端同步失败:", Response.Message); + } + } + if (CallBack) CallBack(Response); + }); +}; + +let PeriodicCloudSync = () => { + if (!CurrentUsername || !UtilityEnabled("CloudSync")) return; + const lastSync = parseInt(localStorage.getItem("UserScript-CloudSync-LastSync") || "0"); + if (Date.now() - lastSync < 60 * 60 * 1000) return; + RequestAPI("GetUserSettings", {}, (Response) => { + if (Response.Success) { + localStorage.setItem("UserScript-CloudSync-LastSync", String(Date.now())); + const cloudSettings = (Response.Data && Response.Data.Settings) || {}; + if (Object.keys(cloudSettings).length > 0) { + let themeChanged = false; + for (let key in cloudSettings) { + const rawValue = String(cloudSettings[key]); + const localKey = "UserScript-Setting-" + key; + if (localStorage.getItem(localKey) !== rawValue) { + localStorage.setItem(localKey, rawValue); + if (key === "Theme") themeChanged = true; + } + } + if (themeChanged) initTheme(); + } + SyncSettingsToCloud(); + } + }); +}; unsafeWindow.GetContestProblemList = async function(RefreshList) { try { @@ -591,7 +644,7 @@ function ConnectNotificationSocket() { return; } - let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-bbs.me") + "/ws/notifications?SessionID=" + Session; + let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-script.uk") + "/ws/notifications?SessionID=" + Session; if (UtilityEnabled("DebugMode")) { console.log("WebSocket: Connecting to", wsUrl); @@ -859,14 +912,19 @@ GM_registerMenuCommand("重置数据", () => { }); //otherwise CurrentUsername might be undefined +let loginStatus; +await fetch("https://www.xmoj.tech/loginpage.php") + .then((response) => response.text()) + .then((data) => (loginStatus = data)); +const logined = loginStatus == "Please logout First!"; if (UtilityEnabled("AutoLogin") && document.querySelector("body > a:nth-child(1)") != null && document.querySelector("body > a:nth-child(1)").innerText == "请登录后继续操作") { localStorage.setItem("UserScript-LastPage", location.pathname + location.search); location.href = "https://www.xmoj.tech/loginpage.php"; } let SearchParams = new URLSearchParams(location.search); -let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me") -if (document.querySelector("#profile") === null) { +let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-script.uk/" : "https://www.xmoj-script.uk") +if (document.querySelector("#profile") === null && !logined) { location.href = "https://www.xmoj.tech/loginpage.php"; } let CurrentUsername = document.querySelector("#profile").innerText; @@ -890,6 +948,8 @@ let initTheme = () => { } }; initTheme(); +PeriodicCloudSync(); +setInterval(PeriodicCloudSync, 60 * 60 * 1000); class NavbarStyler { @@ -1129,7 +1189,7 @@ async function main() { document.querySelector("body > div > div.jumbotron").className = "mt-3"; } - if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php") { + if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php" && !logined) { localStorage.setItem("UserScript-LastPage", location.pathname + location.search); location.href = "https://www.xmoj.tech/loginpage.php"; } @@ -2127,6 +2187,7 @@ async function main() { Select.addEventListener("change", () => { localStorage.setItem("UserScript-Setting-Theme", Select.value); initTheme(); + SyncSettingsToCloud(); }); Row.appendChild(Select); } else if (Data[i].Children == undefined) { @@ -2144,7 +2205,11 @@ async function main() { CheckBox.checked = true; } CheckBox.addEventListener("change", () => { - return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + // Don't sync when disabling CloudSync itself (it's already off) + if (Data[i].ID !== "CloudSync" || CheckBox.checked) { + SyncSettingsToCloud(); + } }); Row.appendChild(CheckBox); @@ -2225,6 +2290,8 @@ async function main() { "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { "ID": "ImageEnlarger", "Type": "A", "Name": "图片放大功能" + }, { + "ID": "CloudSync", "Type": "A", "Name": "将设置同步至云端(跨设备同步)" }, { "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" }, { @@ -2236,6 +2303,88 @@ async function main() { UtilitiesCardBody.appendChild(UtilitiesCardFooter); UtilitiesCard.appendChild(UtilitiesCardBody); Container.appendChild(UtilitiesCard); + let SyncCard = document.createElement("div"); + SyncCard.className = "card mb-3"; + let SyncCardHeader = document.createElement("div"); + SyncCardHeader.className = "card-header"; + SyncCardHeader.innerText = "设置云同步"; + SyncCard.appendChild(SyncCardHeader); + let SyncCardBody = document.createElement("div"); + SyncCardBody.className = "card-body"; + let SyncStatusText = document.createElement("p"); + SyncStatusText.className = "card-text mb-2"; + SyncStatusText.id = "UserScript-SyncStatus"; + SyncStatusText.innerText = "正在从云端加载设置…"; + SyncCardBody.appendChild(SyncStatusText); + let SyncButtonGroup = document.createElement("div"); + SyncButtonGroup.className = "d-flex gap-2"; + let ApplyCloudSettings = (cloudSettings) => { + for (let key in cloudSettings) { + const rawValue = cloudSettings[key]; + localStorage.setItem("UserScript-Setting-" + key, String(rawValue)); + if (key === "Theme") { + let themeSelect = document.getElementById("UserScript-Setting-Theme"); + if (themeSelect) themeSelect.value = String(rawValue); + initTheme(); + } else { + let checkbox = document.getElementById(key); + if (checkbox) { + const normalizedChecked = (typeof rawValue === "boolean") ? rawValue : (String(rawValue).toLowerCase() === "true"); + checkbox.checked = normalizedChecked; + } + } + } + }; + let UploadBtn = document.createElement("button"); + UploadBtn.className = "btn btn-sm btn-primary"; + UploadBtn.innerText = "上传设置到云端"; + UploadBtn.addEventListener("click", () => { + SyncStatusText.innerText = "正在上传…"; + SyncSettingsToCloud((Response) => { + SyncStatusText.innerText = Response.Success ? "上传成功" : ("上传失败: " + Response.Message); + }); + }); + SyncButtonGroup.appendChild(UploadBtn); + let DownloadBtn = document.createElement("button"); + DownloadBtn.className = "btn btn-sm btn-secondary"; + DownloadBtn.innerText = "从云端下载设置"; + DownloadBtn.addEventListener("click", () => { + SyncStatusText.innerText = "正在下载…"; + RequestAPI("GetUserSettings", {}, (Response) => { + if (Response.Success) { + ApplyCloudSettings(Response.Data.Settings); + SyncStatusText.innerText = "下载成功,设置已应用(部分设置需刷新页面后生效)"; + } else { + SyncStatusText.innerText = "下载失败: " + Response.Message; + } + }); + }); + SyncButtonGroup.appendChild(DownloadBtn); + SyncCardBody.appendChild(SyncButtonGroup); + SyncCard.appendChild(SyncCardBody); + Container.appendChild(SyncCard); + if (UtilityEnabled("CloudSync")) { + RequestAPI("GetUserSettings", {}, (Response) => { + let SyncStatusEl = document.getElementById("UserScript-SyncStatus"); + if (Response.Success) { + const cloudSettings = (Response.Data && Response.Data.Settings) || {}; + if (Object.keys(cloudSettings).length === 0) { + if (SyncStatusEl) SyncStatusEl.innerText = "正在上传本地设置至云端…"; + SyncSettingsToCloud((Resp) => { + if (SyncStatusEl) SyncStatusEl.innerText = Resp.Success ? "已将本地设置上传至云端" : ("上传失败: " + Resp.Message); + }); + } else { + ApplyCloudSettings(cloudSettings); + if (SyncStatusEl) SyncStatusEl.innerText = "已从云端加载设置"; + } + } else { + if (SyncStatusEl) SyncStatusEl.innerText = "云端设置加载失败: " + Response.Message; + } + }); + } else { + let SyncStatusEl = document.getElementById("UserScript-SyncStatus"); + if (SyncStatusEl) SyncStatusEl.innerText = "云同步已禁用"; + } let FeedbackCard = document.createElement("div"); FeedbackCard.className = "card mb-3"; let FeedbackCardHeader = document.createElement("div"); @@ -4878,7 +5027,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - Content.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + Content.value = Before + `![](https://assets.xmoj-script.uk/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; Content.dispatchEvent(new Event("input")); } else { Content.value = Before + `![上传失败!` + ResponseData.Message + `]()` + After; @@ -5134,7 +5283,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.value = Before + `![](https://assets.xmoj-script.uk/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentElement.dispatchEvent(new Event("input")); } else { ContentElement.value = Before + `![上传失败!]()` + After; @@ -5307,7 +5456,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.value = Before + `![](https://assets.xmoj-script.uk/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentElement.dispatchEvent(new Event("input")); } else { ContentElement.value = Before + `![上传失败!]()` + After; @@ -5565,7 +5714,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.value = Before + `![](https://assets.xmoj-script.uk/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentEditor.dispatchEvent(new Event("input")); } else { ContentEditor.value = Before + `![上传失败!]()` + After; diff --git a/child-protection.html b/child-protection.html new file mode 100644 index 00000000..f2278440 --- /dev/null +++ b/child-protection.html @@ -0,0 +1,36 @@ + + + + + + 儿童保护协议 - 小明的OJ增强脚本 + + + + +
+

儿童保护协议

+

生效日期:2026-04-24

+ +

一、适用范围

+

本协议适用于未满十四周岁儿童相关信息处理活动,以及监护人对儿童使用服务的管理与监督。

+ +

二、监护人责任

+

监护人应指导并监督儿童合理使用网络服务,避免泄露隐私信息,防止沉迷与不当互动。

+ +

三、儿童信息保护

+

我们坚持最小必要原则处理信息,不会在无正当理由情况下收集与服务无关的儿童个人信息。

+ +

四、风险防护措施

+

我们将持续优化内容展示、交互提醒和异常行为识别机制,降低儿童接触不适宜内容和网络风险的可能性。

+ +

五、监护人权利

+

监护人有权查阅、更正或删除儿童相关信息,并可通过反馈渠道提出限制处理、停止服务等申请。

+ +

六、联系我们

+

如您对儿童信息保护有疑问,请通过项目公开反馈渠道联系我们,我们会及时处理并反馈结果。

+ +

返回首页

+
+ + diff --git a/index.html b/index.html index 522de611..fda3f205 100644 --- a/index.html +++ b/index.html @@ -48,13 +48,14 @@ 关于 + @@ -968,7 +969,16 @@

+
+
我们的网站在萌国备案啦!我们的备案号是:萌ICP备20240425号
+ +
-
我们的网站在萌国备案啦!我们的备案号是:萌ICP备20240425号
diff --git a/messages.html b/messages.html index ea7e2484..f8a8ef86 100644 --- a/messages.html +++ b/messages.html @@ -73,6 +73,9 @@

登录

+ @@ -216,6 +219,22 @@
第二步:在 XMOJ 上点击书签
+ + + @@ -367,8 +386,8 @@ 'use strict'; // ── Constants ────────────────────────────────────────────────────────────── -const API_BASE = 'https://api.xmoj-bbs.me/'; -const ASSET_BASE = 'https://assets.xmoj-bbs.me/GetImage?ImageID='; +const API_BASE = 'https://api.xmoj-script.uk/'; +const ASSET_BASE = 'https://assets.xmoj-script.uk/GetImage?ImageID='; const XMOJ_BASE = 'https://www.xmoj.tech'; const WEBUI_VERSION = 'webui-1.0.0'; const STORAGE_USER = 'xmoj-msg-username'; @@ -377,6 +396,11 @@ const SCROLL_THRESHOLD = 80; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; const PREVIEW_LEN = 60; +const OAUTH_AUTHORIZE_URL = 'https://sso.xmoj-bbs.me/authorize'; +const OAUTH_CLIENT_ID = 'app_0f4cbbddfa42c0c4576cba8f7598d0a2'; +const OAUTH_SCOPE = 'openid profile email xmoj_profile'; +const OAUTH_REDIRECT_URI = 'https://xmoj-bbs.me/messages.html'; +const STORAGE_OAUTH_STATE = 'xmoj-msg-oauth-state'; // ── State ────────────────────────────────────────────────────────────────── let currentUser = null; // { username, phpsessid } @@ -931,6 +955,7 @@ }); document.getElementById('tab-bookmarklet').style.display = tab === 'bookmarklet' ? '' : 'none'; document.getElementById('tab-manual').style.display = tab === 'manual' ? '' : 'none'; + document.getElementById('tab-sso').style.display = tab === 'sso' ? '' : 'none'; } document.querySelectorAll('#loginTabs .nav-link').forEach(function(btn) { @@ -939,6 +964,88 @@ }); }); +function generateUuidV4() { + if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID(); + if (window.crypto && window.crypto.getRandomValues) { + var bytes = new Uint8Array(16); + window.crypto.getRandomValues(bytes); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + var hex = Array.from(bytes).map(function(b) { return b.toString(16).padStart(2, '0'); }).join(''); + return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20)].join('-'); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0; + var v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +function startSsoLogin() { + var state = generateUuidV4() + '.' + generateUuidV4(); + localStorage.setItem(STORAGE_OAUTH_STATE, state); + var params = new URLSearchParams({ + response_type: 'code', + client_id: OAUTH_CLIENT_ID, + redirect_uri: OAUTH_REDIRECT_URI, + scope: OAUTH_SCOPE, + state: state + }); +} + +async function exchangeSsoCode(code, state) { + var res = await fetch(API_BASE + 'ExchangeSSOCode', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Code: code, + State: state, + RedirectURI: OAUTH_REDIRECT_URI, + ClientID: OAUTH_CLIENT_ID + }) + }); + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); +} + +async function handleSsoCallback() { + var url = new URL(location.href); + var code = url.searchParams.get('code'); + var state = url.searchParams.get('state'); + if (!code) return false; + + var expectedState = localStorage.getItem(STORAGE_OAUTH_STATE); + if (!state || !expectedState || state !== expectedState) { + showToast('统一认证登录失败:state 校验失败', 'danger'); + url.searchParams.delete('code'); + url.searchParams.delete('state'); + history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : '')); + return false; + } + + try { + var result = await exchangeSsoCode(code, state); + if (!result || !result.Success || !result.Data) { + throw new Error((result && result.Message) || '后端未返回有效登录数据'); + } + if (!result.Data.Username || !result.Data.SessionID) { + throw new Error('返回数据缺少 Username 或 SessionID'); + } + + localStorage.removeItem(STORAGE_OAUTH_STATE); + saveSession(result.Data.Username, result.Data.SessionID); + url.searchParams.delete('code'); + url.searchParams.delete('state'); + history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : '')); + showToast('统一认证登录成功', 'success'); + onLoggedIn(); + return true; + } catch (err) { + showToast('统一认证登录失败:' + err.message, 'danger'); + return false; + } +} + // ── Event Wiring ─────────────────────────────────────────────────────────── document.getElementById('btn-manual-login').addEventListener('click', function() { var username = document.getElementById('input-username').value.trim(); @@ -948,6 +1055,8 @@ onLoggedIn(); }); +document.getElementById('btn-sso-login').addEventListener('click', startSsoLogin); + document.getElementById('btn-logout').addEventListener('click', logout); document.getElementById('btn-refresh-list').addEventListener('click', loadMailList); @@ -1034,7 +1143,7 @@ }); // ── Boot ─────────────────────────────────────────────────────────────────── -(function init() { +(async function init() { initTheme(); initBookmarklet(); @@ -1050,6 +1159,8 @@ return; } + if (await handleSsoCallback()) return; + if (loadSession()) { onLoggedIn(); } else { diff --git a/package.json b/package.json index 86a7ba56..3cb8e34a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.4.0", + "version": "3.5.0", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { diff --git a/privacy.html b/privacy.html new file mode 100644 index 00000000..3f4aa5f9 --- /dev/null +++ b/privacy.html @@ -0,0 +1,36 @@ + + + + + + 隐私协议 - 小明的OJ增强脚本 + + + + +
+

隐私协议

+

生效日期:2026-04-24

+ +

一、我们收集的信息

+

为提供基础功能,我们可能会处理您在使用脚本时主动提供的数据,包括账号标识、消息内容、页面偏好设置、日志与故障排查信息。

+ +

二、信息使用目的

+

我们仅在实现脚本功能、改进产品体验、保障服务安全与处理用户反馈的必要范围内使用信息,不会将您的信息用于与服务无关的用途。

+ +

三、信息存储与保护

+

我们采取合理的技术与管理措施保护数据安全,尽量减少未授权访问、披露、篡改或丢失风险。数据仅在达到处理目的所需期限内保存。

+ +

四、信息共享与披露

+

除法律法规另有要求,或经您明确同意外,我们不会向无关第三方出售、出租或共享您的个人信息。

+ +

五、您的权利

+

您有权访问、更正、删除相关信息,也可以通过项目反馈渠道申请注销相关服务能力。我们会在合理期限内处理您的请求。

+ +

六、协议更新

+

本协议可能根据功能调整而更新。更新后将通过官网页面公示,继续使用服务即视为您已阅读并同意更新内容。

+ +

返回首页

+
+ + diff --git a/terms.html b/terms.html new file mode 100644 index 00000000..12b52040 --- /dev/null +++ b/terms.html @@ -0,0 +1,36 @@ + + + + + + 服务协议 - 小明的OJ增强脚本 + + + + +
+

服务协议

+

生效日期:2026-04-24

+ +

一、协议说明

+

本协议用于规范您使用小明的OJ增强脚本及相关网页服务的行为。您访问或使用服务即表示同意本协议条款。

+ +

二、服务内容

+

服务包含用户脚本功能、页面增强能力以及相关支持页面。我们会在不降低整体安全性的前提下持续迭代功能。

+ +

三、用户义务

+

您应遵守适用法律法规,不得利用服务从事违法违规活动,不得干扰平台正常运行或侵犯他人合法权益。

+ +

四、知识产权

+

本项目代码及文档受开源协议及相关法律保护。您在使用、修改或分发时需遵守对应许可证条款。

+ +

五、免责与限制

+

在法律允许范围内,服务按“现状”提供。因网络、第三方平台变更、不可抗力或用户自身操作导致的损失,我们不承担超出法律要求的责任。

+ +

六、协议变更与终止

+

我们有权根据业务调整更新协议。若您不同意更新内容,可停止使用服务。您违反本协议时,我们可采取限制措施。

+ +

返回首页

+
+ +