diff --git a/README.md b/README.md
index 32387e6b..2dfc6b00 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,16 @@
请参考 [官网介绍](https://www.xmoj-bbs.me) 。
如果您无法打开该网站,请前往[这里](https://scriptcat.org/zh-CN/script-show-page/1500/)安装。
+### 实验功能:短消息 WebUI
+
+仓库中新增了一个实验页面 `webui.html`,用于在不安装用户脚本时访问短消息相关能力(拉取会话、查看消息、发送消息、粘贴上传图片)。
+
+- 推荐启动代理模式:`node tools/webui-proxy.mjs`,然后访问 `http://127.0.0.1:8787/webui.html`。
+- 代理模式下支持直接输入用户名/密码登录并自动获取 `PHPSESSID`;同时也支持手动粘贴 `PHPSESSID`。
+- 直连模式(直接打开页面)受浏览器跨域限制,账号密码登录可能不可用,此时建议改用代理模式。
+- 安全策略:密码仅用于发起登录请求,不会在前端存储;会话信息仅存储在当前标签页的 `sessionStorage`,可一键清除。
+- 维护策略:API 调用格式与现有脚本中的 `RequestAPI` 保持一致,便于后续复用和统一维护。
+
### 贡献
您想为我们的脚本添砖加瓦吗?快加入我们,为小明的OJ用户创造更美好的环境!(具体要求参见Code Of Conduct)
@@ -105,4 +115,3 @@
-
diff --git a/Update.json b/Update.json
index 38e5a669..d37a21c3 100644
--- a/Update.json
+++ b/Update.json
@@ -2937,7 +2937,7 @@
],
"Notes": "No release notes were provided for this release."
},
- "1.10.0": {
+ "1.999990.0": {
"UpdateDate": 1753443146018,
"Prerelease": false,
"UpdateContents": [
@@ -3424,4 +3424,4 @@
"Notes": "Bug 修复
\n- 修复了暗色模式下比赛排名表(contestrank-oi.php 和 contestrank-correct.php)颜色显示异常的问题(#916)
\n- 修复了 WebSocket 弹窗通知未遵循各功能独立弹窗开关(BBSPopup/MessagePopup)的问题(#919)"
}
}
-}
\ No newline at end of file
+}
diff --git a/index.html b/index.html
index 268a37b2..a5901bc6 100644
--- a/index.html
+++ b/index.html
@@ -41,6 +41,9 @@
安装
+
+ 短消息 WebUI(实验)
+
反馈
diff --git a/tools/webui-proxy.mjs b/tools/webui-proxy.mjs
new file mode 100644
index 00000000..f198a926
--- /dev/null
+++ b/tools/webui-proxy.mjs
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+import { createServer } from 'node:http';
+import { readFile } from 'node:fs/promises';
+import { createHash } from 'node:crypto';
+import { extname, join, normalize } from 'node:path';
+
+const HOST = process.env.HOST || '0.0.0.0';
+const PORT = Number(process.env.PORT || 8787);
+const ROOT = process.cwd();
+
+const MIME = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript; charset=utf-8',
+ '.mjs': 'application/javascript; charset=utf-8',
+ '.css': 'text/css; charset=utf-8',
+ '.json': 'application/json; charset=utf-8',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon'
+};
+
+const MAX_BODY = 15 * 1024 * 1024;
+
+function json(res, code, payload) {
+ res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify(payload));
+}
+
+function parseCookie(setCookie = '') {
+ const match = setCookie.match(/PHPSESSID=([^;]+)/i);
+ return match ? match[1] : '';
+}
+
+function md5(input) {
+ return createHash('md5').update(input).digest('hex');
+}
+
+async function readBody(req) {
+ const chunks = [];
+ let size = 0;
+ for await (const chunk of req) {
+ size += chunk.length;
+ if (size > MAX_BODY) throw new Error('请求体过大');
+ chunks.push(chunk);
+ }
+ if (chunks.length === 0) return {};
+ const raw = Buffer.concat(chunks).toString('utf8');
+ return raw ? JSON.parse(raw) : {};
+}
+
+async function handleLogin(req, res) {
+ const body = await readBody(req);
+ const username = String(body.username || '').trim();
+ const password = String(body.password || '');
+ if (!username || !password) {
+ return json(res, 400, { success: false, message: '用户名或密码不能为空。' });
+ }
+
+ const form = new URLSearchParams();
+ form.set('user_id', username);
+ form.set('password', md5(password));
+
+ const response = await fetch('https://www.xmoj.tech/login.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: form.toString(),
+ redirect: 'manual'
+ });
+
+ const text = await response.text();
+ const setCookie = response.headers.get('set-cookie') || '';
+ const sessionId = parseCookie(setCookie);
+
+ if (!sessionId) {
+ const maybeSuccess = text.includes('history.go(-2);');
+ return json(res, 401, {
+ success: false,
+ message: maybeSuccess ? '登录结果无法解析,请重试。' : '用户名或密码错误。'
+ });
+ }
+
+ return json(res, 200, {
+ success: true,
+ username,
+ sessionId
+ });
+}
+
+async function handleApiProxy(req, res, action) {
+ const body = await readBody(req);
+ const response = await fetch(`https://api.xmoj-bbs.me/${action}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ });
+ const text = await response.text();
+ res.writeHead(response.status, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(text);
+}
+
+async function serveStatic(req, res, pathname) {
+ const clean = normalize(pathname).replace(/^\/+/, '');
+ const file = clean === '' ? 'index.html' : clean;
+ if (file.includes('..')) {
+ res.writeHead(403);
+ res.end('Forbidden');
+ return;
+ }
+ const fullPath = join(ROOT, file);
+ try {
+ const data = await readFile(fullPath);
+ const type = MIME[extname(fullPath)] || 'application/octet-stream';
+ res.writeHead(200, { 'Content-Type': type });
+ res.end(data);
+ } catch {
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+ res.end('Not Found');
+ }
+}
+
+createServer(async (req, res) => {
+ try {
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
+ const pathname = url.pathname;
+
+ if (pathname === '/__webui/health') {
+ return json(res, 200, { success: true, mode: 'proxy' });
+ }
+ if (pathname === '/__webui/login' && req.method === 'POST') {
+ return await handleLogin(req, res);
+ }
+ if (pathname.startsWith('/__webui/api/') && req.method === 'POST') {
+ const action = pathname.substring('/__webui/api/'.length);
+ if (!action) return json(res, 400, { success: false, message: '缺少 action' });
+ return await handleApiProxy(req, res, action);
+ }
+
+ return await serveStatic(req, res, pathname);
+ } catch (error) {
+ return json(res, 500, { success: false, message: error.message || 'Internal error' });
+ }
+}).listen(PORT, HOST, () => {
+ console.log(`[webui-proxy] http://${HOST}:${PORT}`);
+});
diff --git a/webui.html b/webui.html
new file mode 100644
index 00000000..de0d7de4
--- /dev/null
+++ b/webui.html
@@ -0,0 +1,395 @@
+
+
+
+
+
+ XMOJ 短消息 WebUI(实验)
+
+
+
+
+
+
+
XMOJ 短消息 WebUI(实验)
+
密码仅用于单次登录请求,不落盘;会话仅保存在当前标签页的 sessionStorage,可手动清除。
+
正在检测运行模式...
+
+
+
+
1) 登录 / 会话
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
3) 对话内容
+
+
+
请先选择一个会话。
+
+
+
+
+
+
4) 发送消息 / 图片
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+