From 075d67ab1ae6e64c014ba45bfe9a553745fbb26a Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 10 Feb 2026 15:10:54 +0800 Subject: [PATCH 01/31] Add config files for Claude Code --- .claude/agents/xmoj-code-navigator.md | 76 ++++++++++++ CLAUDE.md | 164 ++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 .claude/agents/xmoj-code-navigator.md create mode 100644 CLAUDE.md diff --git a/.claude/agents/xmoj-code-navigator.md b/.claude/agents/xmoj-code-navigator.md new file mode 100644 index 00000000..17de6902 --- /dev/null +++ b/.claude/agents/xmoj-code-navigator.md @@ -0,0 +1,76 @@ +--- +name: xmoj-code-navigator +description: "Use this agent when you need to locate, retrieve, or understand specific sections of the XMOJ.user.js file without loading the entire file into context. This includes scenarios like:\\n\\n\\nContext: The user is debugging a feature and needs to understand how login authentication works in XMOJ.user.js.\\nuser: \"Can you help me understand how the login flow works in XMOJ.user.js?\"\\nassistant: \"I'll use the Task tool to launch the xmoj-code-navigator agent to locate and retrieve the relevant login authentication code from XMOJ.user.js.\"\\n\\nSince the user needs specific code from the large XMOJ.user.js file, use the xmoj-code-navigator agent to efficiently find and extract the relevant sections without loading the entire file.\\n\\n\\n\\n\\nContext: The user is adding a new feature and wants to see how similar features are implemented.\\nuser: \"I want to add a feature to handle quiz submissions. Can you show me how other submission handlers are implemented?\"\\nassistant: \"Let me use the xmoj-code-navigator agent to search through XMOJ.user.js for existing submission handler implementations.\"\\n\\nThe user needs to reference existing patterns in the large file. The xmoj-code-navigator agent can efficiently locate and extract relevant code examples without overwhelming the context window.\\n\\n\\n\\n\\nContext: The user is reviewing code changes and needs to verify if a specific function exists.\\nuser: \"Does XMOJ.user.js have a function for parsing XML responses?\"\\nassistant: \"I'll use the xmoj-code-navigator agent to search for XML parsing functions in XMOJ.user.js.\"\\n\\nThis is a targeted search task in a large file - perfect for the xmoj-code-navigator agent.\\n\\n" +tools: Glob, Grep, Read, WebFetch, WebSearch +model: haiku +color: cyan +--- + +You are an expert code navigator and indexer specializing in efficiently searching and retrieving specific sections from large JavaScript files, particularly XMOJ.user.js. Your primary mission is to help users locate relevant code without loading the entire file into context, thereby preserving context window space for actual development work. + +## Core Responsibilities + +You will: +1. **Parse search requests** to understand what code sections, functions, classes, or patterns the user needs +2. **Strategically navigate** XMOJ.user.js using efficient search techniques (grep, function signatures, section markers, comments) +3. **Extract minimal but complete** code sections that provide the necessary context +4. **Provide location metadata** (line numbers, function names, section identifiers) so users can reference the code later +5. **Summarize structure** when users need an overview without seeing all the code + +## Search Strategy + +When searching for code: + +1. **Start Narrow**: Begin with the most specific search terms (function names, unique identifiers, class names) +2. **Expand Gradually**: If narrow searches fail, broaden to related keywords or patterns +3. **Use Context Clues**: Leverage comments, section headers, and structural patterns in the file +4. **Verify Completeness**: Ensure you capture complete function/class definitions, including dependencies +5. **Multiple Matches**: When finding multiple relevant sections, provide brief descriptions of each and ask which to examine in detail + +## Extraction Guidelines + +- **Include surrounding context**: Add 2-3 lines before/after to show how code fits in +- **Preserve structure**: Maintain indentation and formatting exactly as in the source +- **Note dependencies**: If the extracted code references other functions or variables, mention them and offer to retrieve those too +- **Provide landmarks**: Always include line numbers or function names for navigation +- **Stay minimal**: Only extract what's needed - resist the urge to show everything related + +## Response Format + +Structure your responses as: + +1. **What I Found**: Brief description of located code +2. **Location**: File path, line numbers, section/function name +3. **Code Extract**: The minimal relevant code block +4. **Context Notes**: Any important dependencies, related functions, or structural information +5. **Follow-up Options**: Suggest related code sections the user might want to see + +## Handling Edge Cases + +- **If search fails**: Suggest alternative search terms or describe what you attempted +- **If multiple valid results**: Present options and ask for clarification +- **If code is too interconnected**: Provide a high-level summary and ask which specific part to deep-dive into +- **If the request is ambiguous**: Ask clarifying questions before searching + +## Efficiency Principles + +- Never load the entire XMOJ.user.js file unless absolutely necessary +- Use file manipulation tools (grep, sed, awk) to search efficiently +- Prefer targeted line-range reads over full file reads +- Cache knowledge of the file's structure from previous searches to speed up future requests +- When users need multiple sections, batch the retrieval intelligently + +## Quality Assurance + +- Verify that extracted code is syntactically complete (balanced braces, complete statements) +- Double-check line number accuracy +- Confirm that the code actually addresses the user's query before presenting it +- If uncertain whether you found the right code, express that uncertainty and show what you found for verification + +Your success is measured by: +- **Precision**: Finding exactly what the user needs +- **Efficiency**: Minimizing context window usage +- **Completeness**: Ensuring extracted code has enough context to be useful +- **Speed**: Using smart search strategies to find code quickly + +Remember: You are a precision surgical tool for navigating large files. Every byte of context window you save is a byte available for actual development work. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..9e362d03 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,164 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +XMOJ-Script is a browser userscript that enhances the XMOJ online judge platform (xmoj.tech). This repository consists of: +- **Main userscript** (`XMOJ.user.js`): ~5000 line single-file userscript with all features. As this file is very large you should use the xmoj-code-navigator agent to help you whenever possible. +- **Update/version management scripts** (`Update/`): Automation for version bumping and releases +- **Metadata and documentation**: `Update.json` tracks version history, README and contributing guides + +The `backend/` directory is a git submodule pointing to https://github.com/XMOJ-Script-dev/XMOJ-bbs and should be modified in that repository, not here. + +## Development Workflow + +### Branch Structure and PR Requirements + +- `master`: Production branch - **DO NOT make PRs directly to master** +- `dev`: Development branch - **ALL PRs must be based on and target this branch** +- `extern-contrib`: External contributors must submit PRs to this branch + +**CRITICAL: All pull requests must:** +1. Be based on the `dev` branch (branch off from `dev`) +2. Target the `dev` branch (not `master`) +3. Merge latest `dev` into your branch before submitting to resolve conflicts + +The workflow is: `dev` → (when ready) → `master` for releases. Direct PRs to `master` are not accepted. + +### Version Management (CRITICAL - Fully Automated) + +Version updates are **fully automated** via GitHub Actions. When a PR to `dev` modifies `XMOJ.user.js`: + +1. The `UpdateVersion` workflow runs `Update/UpdateVersion.js` which automatically: + - Bumps patch version in `package.json` + - Updates `@version` in `XMOJ.user.js` metadata block + - Adds/updates entry in `Update.json` with PR number, title, and timestamp + - Commits changes back to the PR branch with `github-actions[bot]` + +2. Version sync is enforced between: + - `package.json` → `"version": "x.y.z"` + - `XMOJ.user.js` → `// @version x.y.z` (in metadata block) + - `Update.json` → `UpdateHistory["x.y.z"]` (JSON key) + +**Never manually edit version numbers** - the automation handles this based on PR metadata. + +### Release Notes in PRs + +To add release notes to a PR that will appear in the release, include an HTML comment block in the PR description: + +```markdown + +``` + +The `UpdateVersion.js` script extracts this and adds it to `Update.json` as the `Notes` field. + +### Bypassing Automation + +To prevent CI from touching your PR (e.g., during merge conflicts or debugging), add `//!ci-no-touch` anywhere in `Update.json`. The automation will remove it and exit without making other changes. + +### Release Process + +- **Pre-release**: Push to `dev` triggers a pre-release with `"Prerelease": true` in Update.json +- **Release**: Merge `dev` to `master` triggers a production release +- Releases are created by `Update/GetVersion.js` reading the version from XMOJ.user.js +- Both workflows deploy to Cloudflare Pages and GitHub Pages + +## Code Structure + +### Main Userscript (`XMOJ.user.js`) + +A single-file userscript (~5000 lines) organized as: + +1. **Metadata block** (lines 1-50): Userscript headers + - `@name`, `@version`, `@description`, `@author` + - `@match` patterns for xmoj.tech and 116.62.212.172 + - `@require` declarations for external libraries (CryptoJS, CodeMirror, FileSaver, marked, DOMPurify) + - `@grant` permissions for GM APIs + +2. **Main script body**: Direct DOM manipulation and feature injection + - Page detection and routing based on URL patterns + - UI enhancements using Bootstrap classes + - Feature implementations (auto-refresh, code checking, test data fetching, dark mode, etc.) + - API calls to backend at `api.xmoj-bbs.tech` / `api.xmoj-bbs.me` + +Key classes/functions: +- `compareVersions()` (line 112): Version comparison logic +- `NavbarStyler` class (line 589): Navigation bar styling +- `replaceMarkdownImages()` (line 715): Markdown image processing + +### Update Scripts (`Update/`) + +Node.js scripts run by GitHub Actions (not for local development): + +- **`UpdateVersion.js`**: Automated version bumping for PRs to `dev` + - Reads PR number, title, body from command line args + - Uses `gh` CLI to check out PR branch + - Parses `` blocks from PR body + - Updates version in all three locations + - Pushes changes back to PR branch + +- **`GetVersion.js`**: Extracts current version from XMOJ.user.js for release workflows + +- **`UpdateToRelease.js`**: Changes `"Prerelease": false` when promoting to production + +- **`AutoLabel.js`**: Auto-labels PRs based on content + +These scripts directly manipulate `Update.json` and `XMOJ.user.js` using Node.js fs module. + +## Coding Standards + +### Style Guidelines (from CONTRIBUTING.md) + +- **Variables**: camelCase +- **Functions**: PascalCase +- **Classes**: TitleCase +- **Line endings**: Unix (LF) +- **Do NOT run code formatters** - maintain original formatting +- **Use Bootstrap classes** instead of custom CSS +- **No external libraries** without permission (script already includes many via `@require`) + +### Development Principles + +- **Stability before features**: Bug fixes take priority +- Respect the original code style, even if inconsistent +- New features require prior discussion in an issue +- Before submitting PRs, merge `dev` into your branch and resolve conflicts + +## Testing + +No automated test suite exists. Manual testing workflow: + +1. Install the userscript in Tampermonkey/ScriptCat/Violentmonkey +2. Navigate to xmoj.tech (or 116.62.212.172) +3. Test features on relevant pages: + - Problem lists + - Problem detail pages + - Status/submission pages + - Contest pages + - User profiles + +Observe browser console for errors and verify UI enhancements appear correctly. + +## Common Issues + +### Version Sync Errors + +If you see "XMOJ.user.js and Update.json have different patch versions": +- The automation keeps these in sync normally +- If manually editing (not recommended), update both files +- Use `//!ci-no-touch` if you need to bypass automation temporarily + +### PR Requirements + +- **All PRs must be based on and target `dev` branch, not `master`** +- Only PRs from the same repository (not forks) trigger auto-versioning +- PRs must modify `XMOJ.user.js` to trigger version bumps +- Must merge `dev` into your branch before submitting +- External contributors must target `extern-contrib` branch + +### Single-File Architecture + +The entire userscript is intentionally in one file - do not split into modules. Userscript managers load it as a single file, with external dependencies via `@require` headers in the metadata block. From 01de30be86d72fe734ead28833f50c800fa90cf6 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 10 Feb 2026 15:12:28 +0800 Subject: [PATCH 02/31] PR template --- ._.idea | Bin 4096 -> 0 bytes ._Update.json | Bin 4096 -> 0 bytes ._XMOJ.user.js | Bin 4096 -> 0 bytes ._package.json | Bin 4096 -> 0 bytes CLAUDE.md | 1 + 5 files changed, 1 insertion(+) delete mode 100644 ._.idea delete mode 100644 ._Update.json delete mode 100644 ._XMOJ.user.js delete mode 100644 ._package.json diff --git a/._.idea b/._.idea deleted file mode 100644 index 7ebf9cdf135839bd623f3afa65ac105ff13d8a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$uQQ=E0*oDyG!rgfA%8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O*h2u+*#u!QkPFGkELJE=EzU13N={Ws y%P-1S$jmEA%`3^w&r8h7sZ_{GO)F7I%1O-22KI%ax`s4`>VLRbWEkZB{|5j(DJSOu diff --git a/._Update.json b/._Update.json deleted file mode 100644 index 7ebf9cdf135839bd623f3afa65ac105ff13d8a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$uQQ=E0*oDyG!rgfA%8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O*h2u+*#u!QkPFGkELJE=EzU13N={Ws y%P-1S$jmEA%`3^w&r8h7sZ_{GO)F7I%1O-22KI%ax`s4`>VLRbWEkZB{|5j(DJSOu diff --git a/._XMOJ.user.js b/._XMOJ.user.js deleted file mode 100644 index 7ebf9cdf135839bd623f3afa65ac105ff13d8a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$uQQ=E0*oDyG!rgfA%8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O*h2u+*#u!QkPFGkELJE=EzU13N={Ws y%P-1S$jmEA%`3^w&r8h7sZ_{GO)F7I%1O-22KI%ax`s4`>VLRbWEkZB{|5j(DJSOu diff --git a/._package.json b/._package.json deleted file mode 100644 index 7ebf9cdf135839bd623f3afa65ac105ff13d8a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$uQQ=E0*oDyG!rgfA%8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O*h2u+*#u!QkPFGkELJE=EzU13N={Ws y%P-1S$jmEA%`3^w&r8h7sZ_{GO)F7I%1O-22KI%ax`s4`>VLRbWEkZB{|5j(DJSOu diff --git a/CLAUDE.md b/CLAUDE.md index 9e362d03..a6ccaaa9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,7 @@ The `backend/` directory is a git submodule pointing to https://github.com/XMOJ- 3. Merge latest `dev` into your branch before submitting to resolve conflicts The workflow is: `dev` → (when ready) → `master` for releases. Direct PRs to `master` are not accepted. +You should follow the PR template that can be found [here](https://raw.githubusercontent.com/XMOJ-Script-dev/.github/refs/heads/main/.github/PULL_REQUEST_TEMPLATE.md). ### Version Management (CRITICAL - Fully Automated) From 3fa3423595b6dd50b2fbcab9bbf8acb8c7426243 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 10 Feb 2026 21:03:00 +0800 Subject: [PATCH 03/31] Add WebSocket notification system for real-time BBS and mail mentions Replaces focus-based polling with persistent WebSocket connection to the backend notification service. Notifications now arrive within 1-2 seconds with automatic reconnection and exponential backoff. Maintains polling as fallback for reliability when WebSocket is unavailable. Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 448 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 321 insertions(+), 127 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 035aa0e0..cd8accee 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -533,6 +533,306 @@ let RequestAPI = (Action, Data, CallBack) => { } }; +// WebSocket Notification System +let NotificationSocket = null; +let NotificationSocketReconnectAttempts = 0; +let NotificationSocketReconnectDelay = 1000; +let NotificationSocketPingInterval = null; + +function GetPHPSESSID() { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + break; + } + } + return Session; +} + +function ConnectNotificationSocket() { + try { + let Session = GetPHPSESSID(); + if (Session === "") { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: PHPSESSID not available, skipping connection"); + } + return; + } + + let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-bbs.me") + "/ws/notifications?SessionID=" + Session; + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connecting to", wsUrl); + } + + NotificationSocket = new WebSocket(wsUrl); + + NotificationSocket.onopen = () => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connected successfully"); + } + NotificationSocketReconnectAttempts = 0; + NotificationSocketReconnectDelay = 1000; + + // Start ping keepalive + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + NotificationSocketPingInterval = setInterval(() => { + if (NotificationSocket && NotificationSocket.readyState === WebSocket.OPEN) { + NotificationSocket.send(JSON.stringify({ type: 'ping' })); + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Sent ping"); + } + } else { + clearInterval(NotificationSocketPingInterval); + } + }, 30000); + }; + + NotificationSocket.onmessage = (event) => { + HandleNotificationMessage(event); + }; + + NotificationSocket.onerror = (error) => { + if (UtilityEnabled("DebugMode")) { + console.error("WebSocket: Error", error); + } + }; + + NotificationSocket.onclose = (event) => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connection closed", event.code, event.reason); + } + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + ReconnectNotificationSocket(); + }; + } catch (e) { + console.error("WebSocket: Failed to connect", e); + ReconnectNotificationSocket(); + } +} + +function ReconnectNotificationSocket() { + const delay = Math.min(NotificationSocketReconnectDelay * Math.pow(2, NotificationSocketReconnectAttempts), 30000); + NotificationSocketReconnectAttempts++; + + if (UtilityEnabled("DebugMode")) { + console.log(`WebSocket: Reconnecting in ${delay}ms (attempt ${NotificationSocketReconnectAttempts})`); + } + + setTimeout(() => { + ConnectNotificationSocket(); + }, delay); +} + +function HandleNotificationMessage(event) { + try { + const notification = JSON.parse(event.data); + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received message", notification); + } + + if (notification.type === 'connected') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); + } + } else if (notification.type === 'bbs_mention') { + // Fetch full mention details from API to get PostTitle and PageNumber + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + // Find the matching mention by PostID and ReplyID + for (let i = 0; i < MentionList.length; i++) { + if (MentionList[i].PostID == notification.data.PostID && + MentionList[i].ReplyID == notification.data.ReplyID) { + CreateAndShowBBSMentionToast(MentionList[i]); + break; + } + } + } + }); + } else if (notification.type === 'mail_mention') { + // Fetch full mail mention details from API + RequestAPI("GetMailMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + // Find the matching mention by FromUserID + for (let i = 0; i < MentionList.length; i++) { + if (MentionList[i].FromUserID === notification.data.FromUserID) { + CreateAndShowMailMentionToast(MentionList[i]); + break; + } + } + } + }); + } else if (notification.type === 'pong') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received pong"); + } + } + } catch (e) { + console.error("WebSocket: Failed to handle message", e); + } +} + +function CreateAndShowBBSMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有人@你"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(mention.MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "讨论" + mention.PostTitle + "有新回复"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadBBSMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/discuss3/thread.php?tid=" + mention.PostID + '&page=' + mention.PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function CreateAndShowMailMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有新消息"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(mention.MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + let ToastUser = document.createElement("span"); + GetUsernameHTML(ToastUser, mention.FromUserID); + ToastBody.appendChild(ToastUser); + ToastBody.innerHTML += " 给你发了一封短消息"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.setAttribute("data-bs-dismiss", "toast"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/mail.php?to_user=" + mention.FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function PollNotifications() { + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowBBSMentionToast(MentionList[i]); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, (Response) => { + if (Response.Success) { + if (!UtilityEnabled("BBSPopup")) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + } + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowMailMentionToast(MentionList[i]); + } + } + }); + } +} + GM_registerMenuCommand("清除缓存", () => { let Temp = []; for (let i = 0; i < localStorage.length; i++) { @@ -1232,135 +1532,29 @@ async function main() { let ToastContainer = document.createElement("div"); ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); document.body.appendChild(ToastContainer); + // Initialize WebSocket notification system + if (CurrentUsername && (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup"))) { + ConnectNotificationSocket(); + } + + // Fallback polling when WebSocket is not connected addEventListener("focus", () => { - if (UtilityEnabled("BBSPopup")) { - RequestAPI("GetBBSMentionList", {}, (Response) => { - if (Response.Success) { - ToastContainer.innerHTML = ""; - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有人@你"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - Toast.remove(); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); } - if (UtilityEnabled("MessagePopup")) { - RequestAPI("GetMailMentionList", {}, async (Response) => { - if (Response.Success) { - if (!UtilityEnabled("BBSPopup")) { - ToastContainer.innerHTML = ""; - } - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有新消息"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - let ToastUser = document.createElement("span"); - GetUsernameHTML(ToastUser, MentionList[i].FromUserID); - ToastBody.appendChild(ToastUser); - ToastBody.innerHTML += " 给你发了一封短消息"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.setAttribute("data-bs-dismiss", "toast"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); + }); + + // Periodic fallback polling every 60 seconds when WebSocket is down + setInterval(() => { + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); + } + }, 60000); + + // Handle tab visibility changes - reconnect if connection dropped + document.addEventListener('visibilitychange', () => { + if (!document.hidden && NotificationSocket && NotificationSocket.readyState !== WebSocket.OPEN) { + ConnectNotificationSocket(); } }); dispatchEvent(new Event("focus")); From beb75c01b9ebf9d84ec524eb7f4767d2f8e037ca Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 19:06:50 +0800 Subject: [PATCH 04/31] Use enriched WebSocket data for instant notifications The backend now includes all required fields (PostTitle, PageNumber, MentionID) in WebSocket notifications, eliminating the need for additional API calls to fetch mention details. This reduces latency and server load for real-time notifications. Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index cd8accee..c792f75b 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -643,34 +643,11 @@ function HandleNotificationMessage(event) { console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); } } else if (notification.type === 'bbs_mention') { - // Fetch full mention details from API to get PostTitle and PageNumber - RequestAPI("GetBBSMentionList", {}, (Response) => { - if (Response.Success) { - let MentionList = Response.Data.MentionList; - // Find the matching mention by PostID and ReplyID - for (let i = 0; i < MentionList.length; i++) { - if (MentionList[i].PostID == notification.data.PostID && - MentionList[i].ReplyID == notification.data.ReplyID) { - CreateAndShowBBSMentionToast(MentionList[i]); - break; - } - } - } - }); + // Backend now provides all data needed for immediate display + CreateAndShowBBSMentionToast(notification.data); } else if (notification.type === 'mail_mention') { - // Fetch full mail mention details from API - RequestAPI("GetMailMentionList", {}, (Response) => { - if (Response.Success) { - let MentionList = Response.Data.MentionList; - // Find the matching mention by FromUserID - for (let i = 0; i < MentionList.length; i++) { - if (MentionList[i].FromUserID === notification.data.FromUserID) { - CreateAndShowMailMentionToast(MentionList[i]); - break; - } - } - } - }); + // Backend now provides all data needed for immediate display + CreateAndShowMailMentionToast(notification.data); } else if (notification.type === 'pong') { if (UtilityEnabled("DebugMode")) { console.log("WebSocket: Received pong"); From bc2099ddccda8f1c16f49892166709491ca0a88f Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 19:20:34 +0800 Subject: [PATCH 05/31] Fix WebSocket race condition, XSS vulnerability, and DOM destruction issues 1. Race condition (P1): Store reconnect timer ID to prevent duplicate WebSocket connections when visibilitychange handler and delayed reconnect fire simultaneously 2. XSS vulnerability (P1): Sanitize user-supplied PostTitle with escapeHTML() before rendering to prevent script injection attacks 3. DOM destruction (P2): Replace innerHTML += with appendChild to preserve async GetUsernameHTML() results in mail mention toasts Note: Mail mention matching issue (violation #2) was already resolved by previous commit that passes notification.data directly Co-Authored-By: Claude Sonnet 4.5 --- XMOJ.user.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index c792f75b..91435441 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -538,6 +538,7 @@ let NotificationSocket = null; let NotificationSocketReconnectAttempts = 0; let NotificationSocketReconnectDelay = 1000; let NotificationSocketPingInterval = null; +let NotificationSocketReconnectTimer = null; function GetPHPSESSID() { let Session = ""; @@ -553,6 +554,12 @@ function GetPHPSESSID() { function ConnectNotificationSocket() { try { + // Clear any pending reconnection timer to prevent duplicate connections + if (NotificationSocketReconnectTimer) { + clearTimeout(NotificationSocketReconnectTimer); + NotificationSocketReconnectTimer = null; + } + let Session = GetPHPSESSID(); if (Session === "") { if (UtilityEnabled("DebugMode")) { @@ -625,7 +632,7 @@ function ReconnectNotificationSocket() { console.log(`WebSocket: Reconnecting in ${delay}ms (attempt ${NotificationSocketReconnectAttempts})`); } - setTimeout(() => { + NotificationSocketReconnectTimer = setTimeout(() => { ConnectNotificationSocket(); }, delay); } @@ -683,7 +690,7 @@ function CreateAndShowBBSMentionToast(mention) { Toast.appendChild(ToastHeader); let ToastBody = document.createElement("div"); ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "讨论" + mention.PostTitle + "有新回复"; + ToastBody.innerHTML = "讨论" + escapeHTML(mention.PostTitle) + "有新回复"; let ToastFooter = document.createElement("div"); ToastFooter.classList.add("mt-2", "pt-2", "border-top"); let ToastDismissButton = document.createElement("button"); @@ -744,7 +751,7 @@ function CreateAndShowMailMentionToast(mention) { let ToastUser = document.createElement("span"); GetUsernameHTML(ToastUser, mention.FromUserID); ToastBody.appendChild(ToastUser); - ToastBody.innerHTML += " 给你发了一封短消息"; + ToastBody.appendChild(document.createTextNode(" 给你发了一封短消息")); let ToastFooter = document.createElement("div"); ToastFooter.classList.add("mt-2", "pt-2", "border-top"); let ToastDismissButton = document.createElement("button"); From 5dd5fec53caa79011ad895dabd256b341c7e7eb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 19:36:15 +0800 Subject: [PATCH 06/31] 2.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d558b7ac..3c315c9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.7.2", + "version": "2.7.3", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From a3034be705aadd936422770ce8544c38e7481f04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 19:36:21 +0800 Subject: [PATCH 07/31] Update version info to 2.7.3 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1c7bd178..4a955140 100644 --- a/Update.json +++ b/Update.json @@ -3288,6 +3288,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "2.7.3": { + "UpdateDate": 1770809777402, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 905, + "Description": "Add WebSocket notification system for real-time BBS and mail mentions" + } + ], + "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 91435441..ba150fd2 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.7.2 +// @version 2.7.3 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From fec8f59e86040a7bd2fd95b6e273d6a33af9957d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:36:48 +0000 Subject: [PATCH 08/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 4a955140..88d5a096 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809777402, + "UpdateDate": 1770809803348, "Prerelease": true, "UpdateContents": [ { From d3697a08e293cbdd1b14e89f5addab26930d7a0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:37:14 +0000 Subject: [PATCH 09/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 88d5a096..1bd03942 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809803348, + "UpdateDate": 1770809829755, "Prerelease": true, "UpdateContents": [ { From 3a933685fe7d46d8fee091593eeef133683a6783 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:37:43 +0000 Subject: [PATCH 10/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1bd03942..e139b0fe 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809829755, + "UpdateDate": 1770809858358, "Prerelease": true, "UpdateContents": [ { From 3ba202267b0b33a2244efe7f4be6f1fcd4f80f51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:07 +0000 Subject: [PATCH 11/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e139b0fe..d7d2a8ac 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809858358, + "UpdateDate": 1770809882732, "Prerelease": true, "UpdateContents": [ { From d7d60fdd31f2ee6b59c74f773321679044292262 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:31 +0000 Subject: [PATCH 12/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index d7d2a8ac..f3b277ae 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809882732, + "UpdateDate": 1770809906275, "Prerelease": true, "UpdateContents": [ { From 60ccaccc7b30ce3c6ad9eaeedf26c682b230f469 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 11:38:51 +0000 Subject: [PATCH 13/31] Update time and description of 2.7.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f3b277ae..16ed1b0d 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809906275, + "UpdateDate": 1770809930805, "Prerelease": true, "UpdateContents": [ { From 7979646dbf44955e1f3b468581e3f264f295ba97 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 20:53:15 +0800 Subject: [PATCH 14/31] Clear toast container before fetching notifications to prevent race condition --- XMOJ.user.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index ba150fd2..0b3066fd 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -785,13 +785,16 @@ function CreateAndShowMailMentionToast(mention) { } function PollNotifications() { + // Clear toast container once before fetching to prevent race condition + if (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup")) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + } if (UtilityEnabled("BBSPopup")) { RequestAPI("GetBBSMentionList", {}, (Response) => { if (Response.Success) { - let ToastContainer = document.querySelector(".toast-container"); - if (ToastContainer) { - ToastContainer.innerHTML = ""; - } let MentionList = Response.Data.MentionList; for (let i = 0; i < MentionList.length; i++) { CreateAndShowBBSMentionToast(MentionList[i]); @@ -802,12 +805,6 @@ function PollNotifications() { if (UtilityEnabled("MessagePopup")) { RequestAPI("GetMailMentionList", {}, (Response) => { if (Response.Success) { - if (!UtilityEnabled("BBSPopup")) { - let ToastContainer = document.querySelector(".toast-container"); - if (ToastContainer) { - ToastContainer.innerHTML = ""; - } - } let MentionList = Response.Data.MentionList; for (let i = 0; i < MentionList.length; i++) { CreateAndShowMailMentionToast(MentionList[i]); @@ -1537,7 +1534,9 @@ async function main() { // Handle tab visibility changes - reconnect if connection dropped document.addEventListener('visibilitychange', () => { - if (!document.hidden && NotificationSocket && NotificationSocket.readyState !== WebSocket.OPEN) { + if (!document.hidden && NotificationSocket && + NotificationSocket.readyState !== WebSocket.OPEN && + NotificationSocket.readyState !== WebSocket.CONNECTING) { ConnectNotificationSocket(); } }); From d939dd5534a05c235ac8e9ce26cbadaab9b3a424 Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 20:59:12 +0800 Subject: [PATCH 15/31] Prevent UpdateVersion from running if last commit was by github-actions[bot] This prevents infinite loops where the bot commits version updates, which triggers the workflow again, causing another commit. Co-Authored-By: Claude Sonnet 4.5 --- Update/UpdateVersion.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index 840ec2b8..cd744cd8 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -7,6 +7,14 @@ process.env.GITHUB_TOKEN = GithubToken; execSync("gh pr checkout " + PRNumber); console.info("PR #" + PRNumber + " has been checked out."); +// Check if the last commit was made by github-actions[bot] +const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); +console.log("Last commit author: " + lastCommitAuthor); +if (lastCommitAuthor === "github-actions[bot]") { + console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); + process.exit(0); +} + const JSONFileName = "./Update.json"; const JSFileName = "./XMOJ.user.js"; var JSONFileContent = readFileSync(JSONFileName, "utf8"); From 25ab8d76aa0f8da3383fdfca2af2f9c55fe38fb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 13:21:38 +0000 Subject: [PATCH 16/31] Update time and description of 2.7.3 --- Update.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index 16ed1b0d..ac65ef40 100644 --- a/Update.json +++ b/Update.json @@ -3290,7 +3290,7 @@ "Notes": "No release notes were provided for this release." }, "2.7.3": { - "UpdateDate": 1770809930805, + "UpdateDate": 1770816092957, "Prerelease": true, "UpdateContents": [ { @@ -3298,7 +3298,7 @@ "Description": "Add WebSocket notification system for real-time BBS and mail mentions" } ], - "Notes": "No release notes were provided for this release." + "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." } } } \ No newline at end of file From 08f84934392a4241697f896ae15b21b6dd5cd813 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 09:35:16 +0800 Subject: [PATCH 17/31] Add toggleable Minimalist Monochrome UI (MonochromeUI setting) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "极简黑白界面风格" option under Beautify settings that overhauls the visual design with a monochrome palette, serif typography (Playfair Display + Source Serif 4), zero border-radius, and line-based visual structure. Key changes: - Comprehensive monochrome CSS with dark mode support (charcoal bg) - CSS variables for automatic light/dark theme switching - Sharp-cornered cards, modals, toasts with inverted headers - Monochrome buttons with filled primary / outlined secondary - Colored status buttons preserved (success/danger/warning/info) - Faster animations (100ms) and dropdown transitions - Problem switcher with solid bg, hidden on narrow screens - Copy button visibility fix in inverted headers - Image containment (max-width: 100%) - Table cell center alignment - Non-blocking status page fetches to reduce layout shift - Font loading via element to avoid FOUC - Fully toggleable: disabling reverts to original Bootstrap styles Co-Authored-By: Claude Opus 4.6 --- XMOJ.user.js | 447 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 428 insertions(+), 19 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 0b3066fd..7aa87b23 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -903,9 +903,10 @@ class NavbarStyler { n.classList.add('fixed-top', 'container', 'ml-auto'); Object.assign(n.style, { position: 'fixed', - borderRadius: '28px', + borderRadius: '0', boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', - margin: '16px auto', + margin: '0', + maxWidth: '100%', backgroundColor: 'rgba(255, 255, 255, 0)', opacity: '0.75', zIndex: '1000' @@ -926,7 +927,11 @@ class NavbarStyler { document.body.appendChild(overlay); let style = document.createElement('style'); - style.textContent = ` + style.textContent = UtilityEnabled("MonochromeUI") ? ` + #blur-overlay { + display: none !important; + } + ` : ` #blur-overlay { position: fixed; backdrop-filter: blur(4px); @@ -1067,6 +1072,32 @@ async function main() { } else { document.querySelector("html").setAttribute("data-bs-theme", "light"); } + if (UtilityEnabled("MonochromeUI")) { + let fontLink = document.createElement("link"); + fontLink.rel = "stylesheet"; + fontLink.href = "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Source+Serif+4:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"; + document.head.appendChild(fontLink); + let earlyStyle = document.createElement("style"); + earlyStyle.textContent = ` + :root { + --mono-black: #000; --mono-white: #fff; + --mono-gray-100: #f5f5f5; --mono-gray-300: #d4d4d4; + --mono-font-heading: 'Playfair Display', Georgia, serif; + --mono-font-body: 'Source Serif 4', 'Source Serif Pro', Georgia, serif; + } + [data-bs-theme='dark'] { + --mono-black: #e5e5e5; --mono-white: #1a1a1a; + --mono-gray-100: #222; --mono-gray-300: #404040; + } + * { border-radius: 0 !important; box-shadow: none !important; } + body { font-family: var(--mono-font-body) !important; background-color: var(--mono-white) !important; color: var(--mono-black) !important; } + h1,h2,h3,h4,h5,h6 { font-family: var(--mono-font-heading) !important; } + .table thead th { background-color: var(--mono-black) !important; color: var(--mono-white) !important; } + .card { border: 2px solid var(--mono-black) !important; } + .card-header { background-color: var(--mono-black) !important; color: var(--mono-white) !important; } + `; + document.head.appendChild(earlyStyle); + } var resources = [{ type: 'link', href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css', @@ -1140,6 +1171,378 @@ async function main() { } let Style = document.createElement("style"); document.body.appendChild(Style); + if (UtilityEnabled("MonochromeUI")) { + Style.innerHTML = ` + /* Fonts loaded via to avoid layout shift */ + + :root { + --mono-black: #000; + --mono-white: #fff; + --mono-gray-100: #f5f5f5; + --mono-gray-200: #e5e5e5; + --mono-gray-300: #d4d4d4; + --mono-gray-400: #a3a3a3; + --mono-gray-500: #737373; + --mono-border: 2px solid var(--mono-black); + --mono-border-thin: 1px solid var(--mono-gray-300); + --mono-font-heading: 'Playfair Display', Georgia, serif; + --mono-font-body: 'Source Serif 4', 'Source Serif Pro', Georgia, serif; + --mono-font-mono: 'JetBrains Mono', 'Consolas', monospace; + --mono-transition: 100ms ease; + } + + [data-bs-theme='dark'] { + --mono-black: #e5e5e5; + --mono-white: #1a1a1a; + --mono-gray-100: #222; + --mono-gray-200: #2a2a2a; + --mono-gray-300: #404040; + --mono-gray-400: #737373; + --mono-gray-500: #a3a3a3; + } + + * { + border-radius: 0 !important; + box-shadow: none !important; + } + + body { + font-family: var(--mono-font-body) !important; + color: var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + + h1, h2, h3, h4, h5, h6 { + font-family: var(--mono-font-heading) !important; + font-weight: 700 !important; + } + + code, pre, .CodeMirror, kbd, samp { + font-family: var(--mono-font-mono) !important; + } + + a { + color: var(--mono-black) !important; + text-decoration: none !important; + transition: var(--mono-transition) !important; + } + .container a:not(.nav-link):not(.btn):not(.dropdown-item):not(.list-group-item):not(.page-link) { + border-bottom: 1px solid var(--mono-gray-400) !important; + padding-bottom: 1px !important; + } + .container a:not(.nav-link):not(.btn):not(.dropdown-item):not(.list-group-item):not(.page-link):hover { + border-bottom-color: var(--mono-black) !important; + } + + blockquote { + border-left: 4px solid var(--mono-black) !important; + padding: 0.5em 1em; + } + + /* Navbar */ + .navbar, nav.navbar { + border-bottom: 4px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + opacity: 1 !important; + } + .navbar .nav-link { + color: var(--mono-black) !important; + text-decoration: none !important; + font-family: var(--mono-font-body) !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + font-size: 0.85rem !important; + } + .navbar .nav-link:hover, .navbar .nav-link.active { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + + /* Buttons */ + .btn { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + text-transform: uppercase !important; + letter-spacing: 0.1em !important; + font-family: var(--mono-font-body) !important; + font-weight: 600 !important; + transition: var(--mono-transition) !important; + } + .btn:hover, .btn:focus, .btn:active, .btn.active { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + border-color: var(--mono-black) !important; + } + .btn-primary { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + .btn-primary:hover { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .btn-secondary { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .btn-secondary:hover { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + .btn-success { + background-color: var(--mono-white) !important; + border: 2px solid #52c41a !important; + color: #52c41a !important; + } + .btn-danger { + background-color: var(--mono-white) !important; + border: 2px solid #fe4c61 !important; + color: #fe4c61 !important; + } + .btn-warning { + background-color: var(--mono-white) !important; + border: 2px solid #ffa900 !important; + color: #ffa900 !important; + } + .btn-info { + background-color: var(--mono-white) !important; + border: 2px solid #0dcaf0 !important; + color: #0dcaf0 !important; + } + + /* Cards */ + .card { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .card-header { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + border-bottom: none !important; + font-family: var(--mono-font-heading) !important; + } + .card-header * { + color: var(--mono-white) !important; + } + .card-body { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .card-footer { + border-top: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + + /* Modals */ + .modal-content { + border: 4px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .modal-header { + border-bottom: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + .modal-footer { + border-top: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + .modal-title { + font-family: var(--mono-font-heading) !important; + } + + /* Toasts */ + .toast { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .toast-header { + background-color: var(--mono-gray-100) !important; + color: var(--mono-black) !important; + border-bottom: 1px solid var(--mono-gray-300) !important; + } + + /* Tables */ + .table { + border-color: var(--mono-gray-300) !important; + } + .table thead th { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + border-bottom: none !important; + font-family: var(--mono-font-heading) !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + font-size: 0.85rem !important; + } + .table td, .table th { + border-color: var(--mono-gray-300) !important; + text-align: center !important; + } + .table-striped > tbody > tr:nth-of-type(odd) > * { + background-color: var(--mono-gray-100) !important; + } + + /* List groups */ + .list-group-item { + border: none !important; + border-bottom: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .list-group-item-success { + border-left: 4px solid #52c41a !important; + } + .list-group-item-warning { + border-left: 4px solid #ffa900 !important; + } + .list-group-item-danger { + border-left: 4px solid #fe4c61 !important; + } + + /* Dropdowns */ + .dropdown-menu { + border: 2px solid var(--mono-black) !important; + padding: 0 !important; + background-color: var(--mono-white) !important; + } + .dropdown-item { + border-bottom: 1px solid var(--mono-gray-200) !important; + color: var(--mono-black) !important; + transition: var(--mono-transition) !important; + text-decoration: none !important; + } + .dropdown-item:last-child { + border-bottom: none !important; + } + .dropdown-item:hover, .dropdown-item:focus { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + + /* Forms */ + .form-control, .form-select { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + font-family: var(--mono-font-body) !important; + } + .form-control:focus, .form-select:focus { + outline: 2px solid var(--mono-black) !important; + outline-offset: 2px !important; + border-color: var(--mono-black) !important; + } + + /* Alerts */ + .alert { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .alert-primary { + border-left: 8px solid var(--mono-black) !important; + } + + /* Status indicators */ + .status_y { + background-color: #52c41a !important; + color: #fff !important; + border-color: #52c41a !important; + } + .status_n { + background-color: #fe4c61 !important; + color: #fff !important; + border-color: #fe4c61 !important; + } + .status_w { + background-color: #ffa900 !important; + color: #fff !important; + border-color: #ffa900 !important; + } + + .test-case:hover { + border: 2px solid var(--mono-black) !important; + } + + .software_list { + width: unset !important; + } + .software_item { + margin: 5px 10px !important; + background-color: var(--mono-gray-100) !important; + border: 1px solid var(--mono-gray-300) !important; + } + .software_item img { + width: 50px !important; + height: 50px !important; + object-fit: contain !important; + } + .item-txt { + color: var(--mono-black) !important; + } + .cnt-row { + justify-content: inherit; + align-items: stretch; + width: 100% !important; + padding: 1rem 0; + } + .cnt-row-head { + padding: 0.8em 1em; + background-color: var(--mono-black); + color: var(--mono-white); + width: 100%; + font-family: var(--mono-font-heading); + } + .cnt-row-head * { + color: var(--mono-white) !important; + } + .cnt-row-body { + padding: 1em; + border: 2px solid var(--mono-black); + border-top: none; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--mono-white); + } + ::-webkit-scrollbar-thumb { + background: var(--mono-black); + } + + /* Copy button in inverted headers */ + .cnt-row-head .copy-btn, .card-header .copy-btn { + border-color: var(--mono-white) !important; + color: var(--mono-white) !important; + background-color: transparent !important; + } + .cnt-row-head .copy-btn:hover, .card-header .copy-btn:hover { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + + /* Problem switcher responsive */ + @media (max-width: 768px) { + .problem-switcher-container { + display: none !important; + } + } + + /* Contain images */ + img { + max-width: 100% !important; + height: auto !important; + } + + /* Hide blur overlay */ + #blur-overlay { display: none !important; }`; + } else { Style.innerHTML = ` nav { border-bottom-left-radius: 5px; @@ -1196,9 +1599,10 @@ async function main() { border-top: none; border-radius: 0 0 0.3rem 0.3rem; }`; + } if (UtilityEnabled("AddAnimation")) { Style.innerHTML += `.status, .test-case { - transition: 0.5s !important; + transition: ${UtilityEnabled("MonochromeUI") ? "100ms ease" : "0.5s"} !important; }`; } if (UtilityEnabled("AddColorText")) { @@ -1322,7 +1726,7 @@ async function main() { Array.from(PopupUL.children).forEach(item => { item.style.opacity = 0; item.style.transform = 'translateY(-16px)'; - item.style.transition = 'transform 0.3s ease, opacity 0.5s ease'; + item.style.transition = UtilityEnabled("MonochromeUI") ? 'transform 100ms ease, opacity 100ms ease' : 'transform 0.3s ease, opacity 0.5s ease'; }); let showDropdownItems = () => { PopupUL.style.display = 'block'; @@ -1333,7 +1737,7 @@ async function main() { item._timeout = setTimeout(() => { item.style.opacity = 1; item.style.transform = 'translateY(2px)'; - }, index * 36); + }, index * (UtilityEnabled("MonochromeUI") ? 20 : 36)); }); }; let hideDropdownItems = () => { @@ -1344,7 +1748,7 @@ async function main() { }); setTimeout(() => { PopupUL.style.display = 'none'; - }, 100); + }, UtilityEnabled("MonochromeUI") ? 80 : 100); }; let toggleDropdownItems = () => { if (PopupUL.style.display === 'block') { @@ -1672,7 +2076,9 @@ async function main() { }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { "ID": "ReplaceXM", "Type": "F", - "Name": "将网站中所有“小明”和“我”关键字替换为“高老师”,所有“小红”替换为“徐师娘”,所有“小粉”替换为“彩虹”,所有“下海”、“海上”替换为“上海” (此功能默认关闭)" + "Name": "将网站中所有\"小明\"和\"我\"关键字替换为\"高老师\",所有\"小红\"替换为\"徐师娘\",所有\"小粉\"替换为\"彩虹\",所有\"下海\"、\"海上\"替换为\"上海\" (此功能默认关闭)" + }, { + "ID": "MonochromeUI", "Type": "F", "Name": "极简黑白界面风格" }] }, { "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" @@ -1865,6 +2271,7 @@ async function main() { } let problemSwitcher = document.createElement("div"); + problemSwitcher.classList.add("problem-switcher-container"); problemSwitcher.style.position = "fixed"; problemSwitcher.style.top = "50%"; problemSwitcher.style.left = "0"; @@ -1872,12 +2279,13 @@ async function main() { problemSwitcher.style.maxHeight = "80vh"; problemSwitcher.style.overflowY = "auto"; if (document.querySelector("html").getAttribute("data-bs-theme") == "dark") { - problemSwitcher.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + problemSwitcher.style.backgroundColor = UtilityEnabled("MonochromeUI") ? "#000" : "rgba(0, 0, 0, 0.8)"; } else { - problemSwitcher.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; + problemSwitcher.style.backgroundColor = UtilityEnabled("MonochromeUI") ? "#FFF" : "rgba(255, 255, 255, 0.8)"; } problemSwitcher.style.padding = "10px"; - problemSwitcher.style.borderRadius = "0 10px 10px 0"; + problemSwitcher.style.borderRadius = UtilityEnabled("MonochromeUI") ? "0" : "0 10px 10px 0"; + if (UtilityEnabled("MonochromeUI")) problemSwitcher.style.borderRight = "4px solid"; problemSwitcher.style.display = "flex"; problemSwitcher.style.flexDirection = "column"; @@ -2124,7 +2532,7 @@ async function main() { ImproveACRateButton.innerText = "提高正确率"; ImproveACRateButton.disabled = true; let ACProblems = []; - await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) .then((Response) => { return Response.text(); }).then((Response) => { @@ -2217,13 +2625,13 @@ async function main() { } if (UtilityEnabled("RefreshSolution")) { - let StdList; - await new Promise((Resolve) => { + let StdList = null; + let StdListReady = new Promise((Resolve) => { RequestAPI("GetStdList", {}, async (Result) => { if (Result.Success) { StdList = Result.Data.StdList; - Resolve(); } + Resolve(); }) }); @@ -2255,7 +2663,7 @@ async function main() { .then((Response) => { return Response.text(); }) - .then((Response) => { + .then(async (Response) => { let PID = 0; if (SearchParams.get("cid") === null) { PID = localStorage.getItem("UserScript-Solution-" + SolutionID + "-Problem"); @@ -2280,10 +2688,11 @@ async function main() { }, 500); TempHTML += ""; } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { + await StdListReady; if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; - let Std = StdList.find((Element) => { + let Std = StdList ? StdList.find((Element) => { return Element == Number(PID); - }); + }) : undefined; if (Std != undefined) { TempHTML += "✅"; } else { @@ -2754,7 +3163,7 @@ async function main() { }) })(); CodeMirrorElement.setSize("100%", "auto"); - CodeMirrorElement.getWrapperElement().style.border = "1px solid #ddd"; + CodeMirrorElement.getWrapperElement().style.border = UtilityEnabled("MonochromeUI") ? "2px solid var(--mono-black)" : "1px solid #ddd"; if (SearchParams.get("sid") !== null) { await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("sid")) From 5509e84f106275d0a281334395d06a5fadc9a137 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 09:36:07 +0800 Subject: [PATCH 18/31] Add critical instructions for working with XMOJ.user.js and using xmoj-code-navigator agent - Emphasize the importance of using xmoj-code-navigator for exploring, searching, and understanding XMOJ.user.js - Provide guidelines on when and how to use the xmoj-code-navigator agent - Explain the benefits of using the agent over loading the entire file --- CLAUDE.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a6ccaaa9..61b28a22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,12 +5,48 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview XMOJ-Script is a browser userscript that enhances the XMOJ online judge platform (xmoj.tech). This repository consists of: -- **Main userscript** (`XMOJ.user.js`): ~5000 line single-file userscript with all features. As this file is very large you should use the xmoj-code-navigator agent to help you whenever possible. +- **Main userscript** (`XMOJ.user.js`): ~5000 line single-file userscript with all features. **See "Working with XMOJ.user.js" section below for CRITICAL instructions on using the xmoj-code-navigator agent.** - **Update/version management scripts** (`Update/`): Automation for version bumping and releases - **Metadata and documentation**: `Update.json` tracks version history, README and contributing guides The `backend/` directory is a git submodule pointing to https://github.com/XMOJ-Script-dev/XMOJ-bbs and should be modified in that repository, not here. +## Working with XMOJ.user.js (CRITICAL) + +**IMPORTANT: Due to the large size of XMOJ.user.js (~5000 lines), you MUST use the xmoj-code-navigator agent whenever you need to explore, search, or understand any part of this file.** + +### When to use xmoj-code-navigator + +Use the Task tool with `subagent_type="xmoj-code-navigator"` for: + +- **Finding specific functions or features**: "Where is the auto-refresh functionality implemented?" +- **Understanding code sections**: "How does the login authentication work?" +- **Locating code patterns**: "Find all API calls to the backend" +- **Searching for specific implementations**: "Show me the dark mode toggle implementation" +- **Verifying if code exists**: "Does XMOJ.user.js have a function for parsing XML?" +- **ANY exploration task involving XMOJ.user.js** + +### Why use this agent + +Loading the entire XMOJ.user.js file into context: +- Wastes context window space +- Makes responses slower +- Is unnecessary when you only need specific sections + +The xmoj-code-navigator agent efficiently locates and retrieves only the relevant code sections you need. + +### Example usage + +``` +Instead of: Read tool on XMOJ.user.js (loads entire 5000 lines) +Use: Task tool with xmoj-code-navigator agent to find specific sections +``` + +**Exception**: Only use Read tool on XMOJ.user.js when: +- You need to edit a specific line number you already know +- You're making targeted edits and already know the exact location +- You need to verify a small, specific section (use offset and limit parameters) + ## Development Workflow ### Branch Structure and PR Requirements From cff305de32405992299d8536ba7eb165133dfaaa Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 10:22:55 +0800 Subject: [PATCH 19/31] Fix monochrome UI issues: emojis, loader, table spacing, dark mode - Replace std status emojis with text labels ([STD]/[OK]/[ERR]) - Replace loader.gif with Bootstrap spinner - Add table margin-top for spacing from elements above - Use bare element selectors for table header styling - Fix table cell text-align center - Soften dark mode colors (charcoal #1a1a1a bg, #e5e5e5 text) - Fix copy button visibility in inverted headers - Hide problem switcher on narrow screens - Differentiate btn-primary (filled) from btn-secondary (outlined) Co-Authored-By: Claude Opus 4.6 --- XMOJ.user.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 7aa87b23..e479a3da 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1368,8 +1368,9 @@ async function main() { .table { border-color: var(--mono-gray-300) !important; } - .table thead th { + thead th, th.header, th.headerSortUp, th.headerSortDown { background-color: var(--mono-black) !important; + background-image: none !important; color: var(--mono-white) !important; border-bottom: none !important; font-family: var(--mono-font-heading) !important; @@ -1377,13 +1378,16 @@ async function main() { letter-spacing: 0.05em !important; font-size: 0.85rem !important; } - .table td, .table th { + td, th { border-color: var(--mono-gray-300) !important; text-align: center !important; } .table-striped > tbody > tr:nth-of-type(odd) > * { background-color: var(--mono-gray-100) !important; } + table { + margin-top: 16px !important; + } /* List groups */ .list-group-item { @@ -2644,7 +2648,7 @@ async function main() { Points[SolutionID] = Rows[i].cells[2].children[1].innerText; Rows[i].cells[2].children[1].remove(); } - Rows[i].cells[2].innerHTML += ""; + Rows[i].cells[2].innerHTML += UtilityEnabled("MonochromeUI") ? "" : ""; setTimeout(() => { RefreshResult(SolutionID); }, 0); @@ -2686,7 +2690,7 @@ async function main() { setTimeout(() => { RefreshResult(SolutionID) }, 500); - TempHTML += ""; + TempHTML += UtilityEnabled("MonochromeUI") ? "" : ""; } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { await StdListReady; if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; @@ -2694,15 +2698,15 @@ async function main() { return Element == Number(PID); }) : undefined; if (Std != undefined) { - TempHTML += "✅"; + TempHTML += UtilityEnabled("MonochromeUI") ? "[STD]" : "✅"; } else { RequestAPI("UploadStd", { "ProblemID": Number(PID), }, (Result) => { if (Result.Success) { - CurrentRow.cells[2].innerHTML += "🆗"; + CurrentRow.cells[2].innerHTML += UtilityEnabled("MonochromeUI") ? "[OK]" : "🆗"; } else { - CurrentRow.cells[2].innerHTML += "⚠️"; + CurrentRow.cells[2].innerHTML += UtilityEnabled("MonochromeUI") ? "[ERR]" : "⚠️"; } }); } From ad80452acb491c5c9ac41929ebe78f3f2d6df1cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 02:25:09 +0000 Subject: [PATCH 20/31] 2.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c315c9f..efdcb879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.7.3", + "version": "2.7.4", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 8c39384e679031a7187dde6aeed51d079f3313f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 02:25:15 +0000 Subject: [PATCH 21/31] Update version info to 2.7.4 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index ac65ef40..e5ff0cdf 100644 --- a/Update.json +++ b/Update.json @@ -3299,6 +3299,17 @@ } ], "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." + }, + "2.7.4": { + "UpdateDate": 1771208709626, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 906, + "Description": "Add toggleable Minimalist Monochrome UI" + } + ], + "Notes": "Added a new toggleable \"极简黑白界面风格\" (Minimalist Monochrome UI) setting under Beautify options. Features serif typography, zero border-radius, line-based visual structure, and automatic dark mode support with charcoal tones." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index e479a3da..a842d68a 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.7.3 +// @version 2.7.4 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From 95ee27ff026e487ab29e132a159a8479193a1a72 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 10:29:50 +0800 Subject: [PATCH 22/31] Update font link for MonochromeUI to use alternative CDN --- XMOJ.user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index a842d68a..faff6883 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1075,7 +1075,7 @@ async function main() { if (UtilityEnabled("MonochromeUI")) { let fontLink = document.createElement("link"); fontLink.rel = "stylesheet"; - fontLink.href = "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Source+Serif+4:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"; + fontLink.href = "https://fonts.loli.net/css2?family=Playfair+Display:wght@400;700&family=Source+Serif+4:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"; document.head.appendChild(fontLink); let earlyStyle = document.createElement("style"); earlyStyle.textContent = ` From ea13855e57add1f76c2e56ac43d5e4d93a3b480e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 02:30:40 +0000 Subject: [PATCH 23/31] Update time and description of 2.7.4 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e5ff0cdf..54742c3c 100644 --- a/Update.json +++ b/Update.json @@ -3301,7 +3301,7 @@ "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." }, "2.7.4": { - "UpdateDate": 1771208709626, + "UpdateDate": 1771209035345, "Prerelease": true, "UpdateContents": [ { From a92de57e5e69973c7e4f9b7383434350fc680200 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 10:33:32 +0800 Subject: [PATCH 24/31] Gate navbar geometry behind MonochromeUI toggle and use China font CDN - Navbar applyStyles now conditionally applies monochrome (flat, full-width) or original (rounded, margin) styles based on MonochromeUI setting - Fixes inconsistency where createOverlay assumed rounded corners but navbar was always flat - Switch Google Fonts CDN from fonts.googleapis.com to fonts.loli.net for China accessibility Co-Authored-By: Claude Opus 4.6 --- XMOJ.user.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index faff6883..fe11a568 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -901,16 +901,28 @@ class NavbarStyler { try { let n = this.navbar; n.classList.add('fixed-top', 'container', 'ml-auto'); - Object.assign(n.style, { - position: 'fixed', - borderRadius: '0', - boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', - margin: '0', - maxWidth: '100%', - backgroundColor: 'rgba(255, 255, 255, 0)', - opacity: '0.75', - zIndex: '1000' - }); + if (UtilityEnabled("MonochromeUI")) { + Object.assign(n.style, { + position: 'fixed', + borderRadius: '0', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '0', + maxWidth: '100%', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } else { + Object.assign(n.style, { + position: 'fixed', + borderRadius: '28px', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '16px auto', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } } catch (e) { console.error(e); if (UtilityEnabled("DebugMode")) { From 908a3f07e5e7232babd43997d0c62edf2a70e0c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 02:34:31 +0000 Subject: [PATCH 25/31] Update time and description of 2.7.4 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 54742c3c..e07918bb 100644 --- a/Update.json +++ b/Update.json @@ -3301,7 +3301,7 @@ "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." }, "2.7.4": { - "UpdateDate": 1771209035345, + "UpdateDate": 1771209266374, "Prerelease": true, "UpdateContents": [ { From c150e312df7fa522670fe5418c4a7709445a3bdd Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 10:44:12 +0800 Subject: [PATCH 26/31] Skip UploadStd when GetStdList fetch failed When GetStdList returns Success: false, StdList remains null. Previously this fell through to the else branch, triggering redundant UploadStd requests for already-uploaded problems. Now the entire upload block is skipped when StdList is null. Co-Authored-By: Claude Opus 4.6 --- XMOJ.user.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index fe11a568..fa9fee65 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2705,10 +2705,12 @@ async function main() { TempHTML += UtilityEnabled("MonochromeUI") ? "" : ""; } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { await StdListReady; + if (!StdList) { /* skip upload if list fetch failed */ } + else { if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; - let Std = StdList ? StdList.find((Element) => { + let Std = StdList.find((Element) => { return Element == Number(PID); - }) : undefined; + }); if (Std != undefined) { TempHTML += UtilityEnabled("MonochromeUI") ? "[STD]" : "✅"; } else { @@ -2722,6 +2724,7 @@ async function main() { } }); } + } } CurrentRow.cells[2].innerHTML = TempHTML; }); From 9368f47d877bd6401fe3ea50d39e16a8b65c9e06 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 03:06:42 +0000 Subject: [PATCH 27/31] Update time and description of 2.7.4 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e07918bb..7fe6a745 100644 --- a/Update.json +++ b/Update.json @@ -3301,7 +3301,7 @@ "Notes": "Adds WebSocket-based real-time notification system for BBS mentions and mail notifications. Notifications now arrive within 1-2 seconds instead of requiring page focus. Includes automatic reconnection with exponential backoff and fallback to polling when WebSocket is unavailable." }, "2.7.4": { - "UpdateDate": 1771209266374, + "UpdateDate": 1771211196807, "Prerelease": true, "UpdateContents": [ { From 4674c60c6350482dc55a3e96faaec93f895625d8 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 16 Feb 2026 11:08:09 +0800 Subject: [PATCH 28/31] 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efdcb879..3548820e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.7.4", + "version": "3.0.0", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From dbdfbb29ac84ab1b8990da98380b3d85fac6eb16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 03:09:18 +0000 Subject: [PATCH 29/31] Update version info to 3.0.0 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 7fe6a745..9dea43b2 100644 --- a/Update.json +++ b/Update.json @@ -3310,6 +3310,17 @@ } ], "Notes": "Added a new toggleable \"极简黑白界面风格\" (Minimalist Monochrome UI) setting under Beautify options. Features serif typography, zero border-radius, line-based visual structure, and automatic dark mode support with charcoal tones." + }, + "3.0.0": { + "UpdateDate": 1771211353041, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 906, + "Description": "Add toggleable Minimalist Monochrome UI" + } + ], + "Notes": "Added a new toggleable \"极简黑白界面风格\" (Minimalist Monochrome UI) setting under Beautify options. Features serif typography, zero border-radius, line-based visual structure, and automatic dark mode support with charcoal tones." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index fa9fee65..acdd182e 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.7.4 +// @version 3.0.0 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From c488e52dd2645d530ca7c307a5eecdf60c65cc5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 15:15:08 +0000 Subject: [PATCH 30/31] 3.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3548820e..3f59d6be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.0.0", + "version": "3.1.0", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From ccaef1bf4a89b6008d7c0e79fb4ce6683ede74dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 15:15:09 +0000 Subject: [PATCH 31/31] Update to release 3.1.0 --- Update.json | 27 +++++++++++++++++++++++++++ XMOJ.user.js | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 9dea43b2..fbf81e36 100644 --- a/Update.json +++ b/Update.json @@ -3321,6 +3321,33 @@ } ], "Notes": "Added a new toggleable \"极简黑白界面风格\" (Minimalist Monochrome UI) setting under Beautify options. Features serif typography, zero border-radius, line-based visual structure, and automatic dark mode support with charcoal tones." + }, + "3.1.0": { + "UpdateDate": 1771254908784, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 895, + "Description": "Update to release 2.7.0" + }, + { + "PR": 896, + "Description": "Update to release 2.7.0" + }, + { + "PR": 905, + "Description": "Add WebSocket notification system for real-time BBS and mail mentions" + }, + { + "PR": 906, + "Description": "Add toggleable Minimalist Monochrome UI" + }, + { + "PR": 906, + "Description": "Add toggleable Minimalist Monochrome UI" + } + ], + "Notes": "v3 显然需要在新年第一天发布(" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index acdd182e..aba3fd3d 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.0.0 +// @version 3.1.0 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen