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 @@ + 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) 登录 / 会话

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+

2) 会话列表

+ +
+
+
+
+
+
+
+
+
+

3) 对话内容

+ +
+
请先选择一个会话。
+
+
+
+
+
+

4) 发送消息 / 图片

+
+ +
+
+ + +
+
+
+
+
+
+ + + +