From 2d575e2e842bcbcf7a79679aba2be584050e5905 Mon Sep 17 00:00:00 2001 From: Bluemangoo Date: Thu, 8 May 2025 20:05:49 +0800 Subject: [PATCH] Initial Commit --- .gitignore | 7 + .prettierrc.json | 5 + package.json | 21 +++ pnpm-lock.yaml | 240 ++++++++++++++++++++++++++++ script/buct-cource-copy.js | 146 +++++++++++++++++ script/ds-listener.js | 155 ++++++++++++++++++ script/tempermonkey.d.ts | 308 ++++++++++++++++++++++++++++++++++++ src/app/app.ts | 14 ++ src/app/data/polls.ts | 45 ++++++ src/app/initRouter.ts | 4 + src/app/router.ts | 4 + src/app/routers/answer.ts | 27 ++++ src/app/routers/polling.ts | 48 ++++++ src/app/routers/question.ts | 33 ++++ src/app/routers/test.ts | 31 ++++ src/const.ts | 5 + src/main.ts | 11 ++ src/utils/poll.ts | 27 ++++ tsconfig.json | 10 ++ 19 files changed, 1141 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 script/buct-cource-copy.js create mode 100644 script/ds-listener.js create mode 100644 script/tempermonkey.d.ts create mode 100644 src/app/app.ts create mode 100644 src/app/data/polls.ts create mode 100644 src/app/initRouter.ts create mode 100644 src/app/router.ts create mode 100644 src/app/routers/answer.ts create mode 100644 src/app/routers/polling.ts create mode 100644 src/app/routers/question.ts create mode 100644 src/app/routers/test.ts create mode 100644 src/const.ts create mode 100644 src/main.ts create mode 100644 src/utils/poll.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48743e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Project Configs +.idea + +# Node Modules +node_modules + +# Cache diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..69d248e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "trailingComma": "none", + "tabWidth": 4 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..297eadd --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "buct-cource-bridge", + "version": "1.0.0", + "main": "index.js", + "author": "Bluemangoo", + "private": true, + "description": "", + "scripts": { + "start": "ts-node src/main.ts" + }, + "devDependencies": { + "@types/node": "^22.15.14", + "prettier": "^3.5.3", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "dependencies": { + "@vclight/router": "^1.5.1", + "vclight": "^3.3.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..330ddc7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,240 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@vclight/router': + specifier: ^1.5.1 + version: 1.5.1 + vclight: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@types/node': + specifier: ^22.15.14 + version: 22.15.14 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.15.14)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + +packages: + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/node@22.15.14': + resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==} + + '@vclight/router@1.5.1': + resolution: {integrity: sha512-oLojdFEagVZtjomTLdTqg+MSV72YQAzoBxNsWr/fCCyPHxvZe2G5/lqc2KdDqIBpwCGkeMZP5n2AK03BnD51eg==} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie@0.7.0: + resolution: {integrity: sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==} + engines: {node: '>= 0.6'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vclight@3.3.2: + resolution: {integrity: sha512-ZQ5pWzTo9iRblGL8WikUPT4O7MKl0hSSj/Tu/NMmFv1XCF04lfydXG0yDOoTcvLI5JvAawpABKH/anmhmTcjrw==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/node@22.15.14': + dependencies: + undici-types: 6.21.0 + + '@vclight/router@1.5.1': + dependencies: + cookie: 0.7.0 + vclight: 3.3.2 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + arg@4.1.3: {} + + content-type@1.0.5: {} + + cookie@0.7.0: {} + + create-require@1.1.1: {} + + diff@4.0.2: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + etag@1.8.1: {} + + make-error@1.3.6: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + prettier@3.5.3: {} + + ts-node@10.9.2(@types/node@22.15.14)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.14 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + v8-compile-cache-lib@3.0.1: {} + + vclight@3.3.2: + dependencies: + content-type: 1.0.5 + cookie: 0.7.0 + end-of-stream: 1.4.4 + etag: 1.8.1 + + wrappy@1.0.2: {} + + yn@3.1.1: {} diff --git a/script/buct-cource-copy.js b/script/buct-cource-copy.js new file mode 100644 index 0000000..48c923b --- /dev/null +++ b/script/buct-cource-copy.js @@ -0,0 +1,146 @@ +// ==UserScript== +// @name BUCT cource helper +// @namespace http://tampermonkey.net/ +// @version 2025-03-12 +// @description 北化在线助手 +// @author Bluemangoo +// @match https://course.buct.edu.cn/meol/jpk/course/layout/newpage/index.jsp?* +// @icon https://www.google.com/s2/favicons?sz=64&domain=buct.edu.cn +// @grant GM_registerMenuCommand +// @grant GM_unregisterMenuCommand +// ==/UserScript== + +(function () { + "use strict"; + + const HOST = "https://local.bluemangoo.net:3443"; + + let question = { + id: "" + }; + + async function poll() { + let res; + try { + res = await fetch(`${HOST}/polling?env=course`, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + } catch { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return poll(); + } + poll(); + try { + const data = await res.json(); + if (data.status !== "nothing") { + const { questionID, answer } = data.data; + console.log("回答", questionID, "\n", answer); + answerQuestion(questionID, answer); + } + } catch (e) { + console.error(e); + } + } + + function getAnswerElement() { + const doc = document + .getElementById("mainFrame") + .contentDocument.getElementById("questionshow").contentDocument; + return doc.getElementsByClassName("extable")[0]; + } + + function getContent() { + const doc = document + .getElementById("mainFrame") + .contentDocument.getElementById("questionshow").contentDocument; + const question = doc + .getElementsByTagName("iframe")[0] + .contentDocument.getElementById("body").innerText; + const answer = doc.getElementsByClassName("extable")[0].innerText.replaceAll("\t", "- "); + return question + answer; + } + + function copyToClipboard() { + navigator.clipboard.writeText(getContent()); + } + + function answerQuestion(id, answer) { + if (id !== question.id) { + return; + } + const answers = answer.split("\n"); + const children = getAnswerElement().children[0].children; + for (const child of children) { + if (child.classList[0] !== "optionContent") { + continue; + } + if (answers.includes(child.innerText.replaceAll("\t", ""))) { + child.children[0].children[0].click(); + } + } + } + + async function ask() { + const q = getContent(); + const req = await fetch(`${HOST}/question`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + question: q + }) + }); + const data = await req.json(); + if (data.data.questionId) { + question.id = data.data.questionId; + } + } + + const flag = { + copyButton: false, + aiButton: false + }; + + let aiButton; + + function createButton(addAiButton = false) { + const base = document.getElementById("mainFrame"); + const win = base.contentWindow; + const doc = base.contentDocument; + win.getContent = getContent; + win.getAnswerElement = getAnswerElement; + win.answerQuestion = answerQuestion; + win.copyToClipboard = copyToClipboard; + if (!flag.copyButton) { + const button = doc.createElement("input"); + button.value = "复制"; + button.style = "margin-left: 10px;"; + button.type = "button"; + button.onclick = copyToClipboard; + doc.getElementsByClassName("navigation")[0].childNodes[1].appendChild(button); + flag.copyButton = true; + } + if (addAiButton && !flag.aiButton) { + const button = doc.createElement("input"); + aiButton = button; + button.value = "?"; + button.style = "margin-left: 10px;"; + button.type = "button"; + button.onclick = ask; + doc.getElementsByClassName("navigation")[0].childNodes[1].appendChild(button); + flag.aiButton = true; + } + } + + function initAiBridge() { + createButton(true); + poll(); + } + + GM_registerMenuCommand("添加复制按钮", createButton); + GM_registerMenuCommand("初始化ai桥", initAiBridge); +})(); diff --git a/script/ds-listener.js b/script/ds-listener.js new file mode 100644 index 0000000..f5e4686 --- /dev/null +++ b/script/ds-listener.js @@ -0,0 +1,155 @@ +// ==UserScript== +// @name BUCT cource deepseek listener +// @namespace http://tampermonkey.net/ +// @version 2025-05-07 +// @description try to take over the world! +// @author You +// @match https://chat.deepseek.com/* +// @icon https://www.google.com/s2/favicons?sz=64&domain=deepseek.com +// @grant GM_registerMenuCommand +// @grant GM_unregisterMenuCommand +// ==/UserScript== + +(function () { + "use strict"; + const CLASS_MAP = { + input: "_27c9245", + send: "_7436101", + chat: "dad65929", + latest: "d7dc56a8", + done: "_43c05b5" + }; + + const HOST = "https://local.bluemangoo.net:3443"; + + const question = { + question: "", + questionID: "", + answered: true + }; + + async function poll() { + // query latest question + let res; + try { + res = await fetch(`${HOST}/polling?env=deepseek`, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + } catch { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return poll(); + } + poll(); + try { + const data = await res.json(); + if (data.status !== "nothing") { + if (question.answered) { + question.answered = false; + question.question = data.data.question; + question.questionID = data.data.questionID; + console.log("获取到新问题:", question.question); + ask(question.question); + } + } + } catch (e) { + console.error(e); + } + } + + async function answer(questionID, answer) { + console.log("回答问题:", questionID, answer); + question.answered = true; + await fetch(`${HOST}/answer`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + questionID, + answer + }) + }); + } + + function setNativeValue(element, value) { + const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set; + const prototype = Object.getPrototypeOf(element); + const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, "value").set; + + if (valueSetter && valueSetter !== prototypeValueSetter) { + prototypeValueSetter.call(element, value); + } else { + valueSetter.call(element, value); + } + } + + function ask(question) { + const input = document.getElementsByClassName(CLASS_MAP.input)[0]; + setNativeValue(input, question); + input.dispatchEvent(new Event("input", { bubbles: true })); + document.getElementsByClassName(CLASS_MAP.send)[0].click(); + } + + function init() { + const chat = document.getElementsByClassName(CLASS_MAP.chat)[0]; + + // 创建一个 MutationObserver 来监听子元素变化 + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // 检查新增的节点 + mutation.addedNodes.forEach((node) => { + // 确保是元素节点 + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node; + // 检查新增的元素是否同时包含 "_4f9bf79" 和 "d7dc56a8" 类 + if ( + element.classList.contains("_4f9bf79") && + element.classList.contains("d7dc56a8") + ) { + console.log("目标元素已添加:", element); + + // 对这个元素添加属性变化的监听器 + const targetObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // 检查是否添加了 "_43c05b5" 类 + if ( + mutation.type === "attributes" && + mutation.attributeName === "class" && + element.classList.contains("_43c05b5") + ) { + const ans = element.innerText.toString(); + console.log("内容:", ans); + // 可以在这里执行其他操作 + targetObserver.disconnect(); + answer(question.questionID, ans); + } + }); + }); + + // 开始观察目标元素的属性变化 + targetObserver.observe(element, { + attributes: true, // 监听属性变化 + attributeFilter: ["class"] // 只监听 class 变化 + }); + } + } + }); + }); + }); + + // 开始观察父元素的子元素变化 + observer.observe(chat, { + childList: true, // 监听子元素变化 + subtree: true // 监听所有后代元素 + }); + + console.log("正在监听父元素:", chat); + + poll(); + } + + GM_registerMenuCommand("初始化作业桥", init); +})(); diff --git a/script/tempermonkey.d.ts b/script/tempermonkey.d.ts new file mode 100644 index 0000000..b317462 --- /dev/null +++ b/script/tempermonkey.d.ts @@ -0,0 +1,308 @@ +/** +unsafeWindow 对象提供对页面 javascript 函数和变量的完全访问。 +*/ +declare var unsafeWindow: Window; + +/** +获取有关脚本和 TM 的一些信息。 +*/ +declare var GM_info: { + version: string; + scriptWillUpdate: boolean; + scriptHandler: "Tampermonkey"; + scriptUpdateURL?: string; + scriptSource: string; + scriptMetaStr?: string; + isIncognito: boolean; + downloadMode: "native" | "disabled" | "browser"; + script: { + author?: string; + description?: string; + excludes: string[]; + homepage?: string; + icon?: string; + icon64?: string; + includes?: string[]; + lastModified: number; + matches: string[]; + name: string; + namespace?: string; + position: number; + "run-at": string; + resources: string[]; + unwrap: boolean; + version: string; + options: { + awareOfChrome: boolean; + run_at: string; + noframes?: boolean; + compat_arrayLeft: boolean; + compat_foreach: boolean; + compat_forvarin: boolean; + compat_metadata: boolean; + compat_uW_gmonkey: boolean; + override: { + orig_excludes: string[]; + orig_includes: string[]; + use_includes: string[]; + use_excludes: string[]; + [key: string]: any; + }; + [key: string]: any; + }; + [key: string]: any; + }; + [key: string]: any; +}; + +/** +将给定的样式添加到文档并返回注入的样式元素。 +*/ +declare function GM_addStyle(css: string): void; + +/** +创建一个由“tag_name”指定的 HTML 元素并应用所有给定的“属性”并返回注入的 HTML 元素。 +*/ +declare function GM_addElement(tag_name: string, attributes: object); + +/** +创建一个由“tag_name”指定的 HTML 元素并应用所有给定的“属性”并返回注入的 HTML 元素。如果给出了“parent_node”,则将其附加到它或以其他方式附加到文档头或体。 +*/ +declare function GM_addElement(parent_node: HTMLElement, tag_name: string, attributes: object); + +/** +从存储中删除“名称”。 +*/ +declare function GM_deleteValue(name: string): void; + +/** +列出存储的所有名称。 +*/ +declare function GM_listValues(): string[]; + +/** +将更改侦听器添加到存储并返回侦听器 ID。 +'name' 是观察变量的名称。 +回调函数的“remote”参数显示该值是从另一个选项卡的实例 (true) 还是在此脚本实例 (false) 中修改的。 +因此,不同浏览器选项卡的脚本可以使用此功能相互通信。 +*/ +declare function GM_addValueChangeListener( + name: string, + listener: GM_Types.ValueChangeListener +): number; + +/** +按 ID 删除更改​​侦听器。 +*/ +declare function GM_removeValueChangeListener(listenerId: number): void; + +/** +设置本地存储'name'的值。 +*/ +declare function GM_setValue(name: string, value: any): void; + +/** +从存储中获取 'name' 的值。 +*/ +declare function GM_getValue(name: string, defaultValue?: any): any; + +/** +将消息打印到控制台。 +*/ +declare function GM_log(message: string): any; + +/** +获取脚本头中预定义的@resource 标记的内容。 +*/ +declare function GM_getResourceText(name: string): string; + +/** +获取脚本标头处预定义 @resource 标记的 base64 编码 URI。 +*/ +declare function GM_getResourceURL(name: string): string; + +/** +在运行此脚本的页面的 Tampermonkey 菜单中注册要显示的菜单,并返回菜单命令 ID。 +*/ +declare function GM_registerMenuCommand( + name: string, + listener: Function, + accessKey?: string +): number; + +/** +使用给定的菜单命令 ID 取消注册先前由 GM_registerMenuCommand 注册的菜单命令。 +*/ +declare function GM_unregisterMenuCommand(id: number): void; + +/** +使用此 url 打开一个新选项卡。 +*/ +declare function GM_openInTab(url: string, options: GM_Types.OpenTabOptions): void; +/** +使用此 url 打开一个新选项卡。 +*/ +declare function GM_openInTab(url: string, loadInBackground: boolean): void; +/** +使用此 url 打开一个新选项卡。 +*/ +declare function GM_openInTab(url: string): void; + +/** +创建一个 xmlHttpRequest。 +*/ +declare function GM_xmlhttpRequest( + details: GM_Types.XHRDetails +): GM_Types.AbortHandle; + +/** +将给定的 URL 下载到本地磁盘。 +*/ +declare function GM_download(details: GM_Types.DownloadDetails): GM_Types.AbortHandle; +/** +将给定的 URL 下载到本地磁盘。 +*/ +declare function GM_download(url: string, filename: string): GM_Types.AbortHandle; + +/** +只要此选项卡处于打开状态,就获取一个持久对象。 +*/ +declare function GM_getTab(callback: (obj: object) => any): void; + +/** +保存选项卡对象以在页面卸载后重新打开它。 +*/ +declare function GM_saveTab(obj: object): void; + +/** +获取所有选项卡对象作为散列以与其他脚本实例通信。 +*/ +declare function GM_getTabs(callback: (objs: { [key: number]: object }) => any): void; + +/** +显示 HTML5 桌面通知和/或突出显示当前选项卡。 +*/ +declare function GM_notification(details: GM_Types.NotificationDetails, ondone: Function): void; +/** +显示 HTML5 桌面通知和/或突出显示当前选项卡。 +*/ +declare function GM_notification( + text: string, + title: string, + image: string, + onclick: Function +): void; + +/** +将数据复制到剪贴板。参数 'info' 可以是像“{ type: 'text', mimetype: 'text/plain'}”这样的对象,或者只是一个表示类型的字符串(“text”或“html”)。 +*/ +declare function GM_setClipboard( + data: string, + info?: string | { type?: string; mimetype?: string } +): void; + +declare namespace GM_Types { + type ValueChangeListener = (name: string, oldValue: any, newValue: any, remote: boolean) => any; + + interface OpenTabOptions { + active?: boolean; + insert?: boolean; + setParent?: boolean; + } + + interface XHRResponse extends Function { + DONE: 4; + HEADERS_RECEIVED: 2; + LOADING: 3; + OPENED: 1; + UNSENT: 0; + + context: CONTEXT_TYPE; + finalUrl: string; + readyState: 0 | 1 | 2 | 3 | 4; + responseHeaders: string; + status: number; + statusText: string; + response: string | null; + responseText: string; + responseXML: Document | null; + } + + interface XHRProgress extends XHRResponse { + done: number; + lengthComputable: boolean; + loaded: number; + position: number; + total: number; + totalSize: number; + } + + type Listener = (this: OBJ, event: OBJ) => any; + + interface XHRDetails { + method?: "GET" | "HEAD" | "POST"; + url?: string; + headers?: { readonly [key: string]: string }; + data?: string; + binary?: boolean; + timeout?: number; + context?: CONTEXT_TYPE; + responseType?: "arraybuffer" | "blob" | "json"; + overrideMimeType?: string; + anonymous?: boolean; + fetch?: boolean; + username?: string; + password?: string; + + onload?: Listener>; + onloadstart?: Listener>; + onprogress?: Listener>; + onreadystatechange?: Listener>; + ontimeout?: Listener; + onabort?: Function; + onerror?: Function; + } + + interface AbortHandle { + abort(): RETURN_TYPE; + } + + interface DownloadError { + error: + | "not_enabled" + | "not_whitelisted" + | "not_permitted" + | "not_supported" + | "not_succeeded"; + details?: string; + } + + interface DownloadDetails { + url: string; + name: string; + headers?: { readonly [key: string]: string }; + saveAs?: boolean; + timeout?: number; + onerror?: Listener; + ontimeout?: Listener; + onload?: Listener; + onprogress?: Listener>; + } + + interface NotificationThis extends NotificationDetails { + id: string; + } + + type NotificationOnClick = (this: NotificationThis) => any; + type NotificationOnDone = (this: NotificationThis, clicked: boolean) => any; + + interface NotificationDetails { + text?: string; + title?: string; + image?: string; + highlight?: boolean; + timeout?: number; + onclick?: NotificationOnClick; + ondone?: NotificationOnDone; + } +} diff --git a/src/app/app.ts b/src/app/app.ts new file mode 100644 index 0000000..8b6c6b1 --- /dev/null +++ b/src/app/app.ts @@ -0,0 +1,14 @@ +import VCLight, { VCLightApp, VCLightRequest, VCLightResponse } from "vclight"; +import router from "./router"; +import "./initRouter"; + +const app = new VCLight(); +app.use(router); +app.use({ + async post(request: VCLightRequest, response: VCLightResponse, app: VCLightApp) {}, + async process(request: VCLightRequest, response: VCLightResponse, app: VCLightApp) { + response.headers["access-control-allow-origin"] = "*"; + response.headers["access-control-allow-headers"] = "*"; + } +}); +export default app; diff --git a/src/app/data/polls.ts b/src/app/data/polls.ts new file mode 100644 index 0000000..9b59924 --- /dev/null +++ b/src/app/data/polls.ts @@ -0,0 +1,45 @@ +import Poll from "../../utils/poll"; +import CONST from "../../const"; + +type Polls = { value: Poll[]; cache: T }; + +export const coursePolls: Polls = { + value: [], + cache: { + questionID: "", + answer: "", + flag: false + } +}; +export const deepSeekPolls: Polls = { + value: [], + cache: { + questionID: "", + question: "", + flag: false + } +}; + +export interface CoursePollData { + questionID: string; + answer: string; + flag: boolean; +} + +export interface DeepSeekPollData { + questionID: string; + question: string; + flag: boolean; +} + +export function pollCourse() { + const poll = new Poll(CONST.pollInterval); + coursePolls.value.push(poll); + return poll.wait(); +} + +export function pollDeepSeek() { + const poll = new Poll(CONST.pollInterval); + deepSeekPolls.value.push(poll); + return poll.wait(); +} diff --git a/src/app/initRouter.ts b/src/app/initRouter.ts new file mode 100644 index 0000000..e204722 --- /dev/null +++ b/src/app/initRouter.ts @@ -0,0 +1,4 @@ +import "./routers/answer"; +import "./routers/polling"; +import "./routers/question"; +import "./routers/test"; diff --git a/src/app/router.ts b/src/app/router.ts new file mode 100644 index 0000000..b08a81a --- /dev/null +++ b/src/app/router.ts @@ -0,0 +1,4 @@ +import VCLightRouter from "@vclight/router"; + +const router = new VCLightRouter(); +export default router; diff --git a/src/app/routers/answer.ts b/src/app/routers/answer.ts new file mode 100644 index 0000000..4d5a410 --- /dev/null +++ b/src/app/routers/answer.ts @@ -0,0 +1,27 @@ +import router from "../router"; +import { coursePolls } from "../data/polls"; + +router.on("/answer", async function (data, response) { + console.log(data.body); + const { questionID, answer } = data.body; + response.contentType = "application/json"; + if (typeof questionID != "string" || typeof answer != "string") { + response.response = { + status: "error", + message: "questionID and answer are required" + }; + return; + } + console.log("Received answer", questionID, answer); + const polls = coursePolls.value; + const pollData = { + questionID: questionID, + answer: answer, + flag: true + }; + coursePolls.value = []; + polls.forEach((poll) => { + poll.resolve(pollData); + }); + coursePolls.cache = pollData; +}); diff --git a/src/app/routers/polling.ts b/src/app/routers/polling.ts new file mode 100644 index 0000000..7c9a2ca --- /dev/null +++ b/src/app/routers/polling.ts @@ -0,0 +1,48 @@ +import router from "../router"; +import { + CoursePollData, + coursePolls, + DeepSeekPollData, + deepSeekPolls, + pollCourse, + pollDeepSeek +} from "../data/polls"; +import Poll from "../../utils/poll"; + +router.on("/polling", async function (data, response) { + if (data.method == "OPTIONS") { + return; + } + if (data.query.env == "course" || data.query.env == "deepseek") { + const polls = data.query.env == "course" ? coursePolls : deepSeekPolls; + + if (polls.cache.flag) { + response.response = { + status: "success", + data: JSON.parse(JSON.stringify(polls.cache)) + }; + polls.cache.flag = false; + console.log("Polling cache", polls.cache); + return; + } + + let pollData: CoursePollData | DeepSeekPollData | typeof Poll.TIMEOUT = await (data.query + .env == "course" + ? pollCourse() + : pollDeepSeek()); + response.contentType = "application/json"; + if (pollData == Poll.TIMEOUT) { + response.response = { + status: "nothing" + }; + return; + } + pollData = pollData; + pollData.flag = false; + response.response = { + status: "success", + data: pollData + }; + console.log("Polling data", pollData); + } +}); diff --git a/src/app/routers/question.ts b/src/app/routers/question.ts new file mode 100644 index 0000000..e48222f --- /dev/null +++ b/src/app/routers/question.ts @@ -0,0 +1,33 @@ +import router from "../router"; +import { coursePolls, deepSeekPolls } from "../data/polls"; + +router.on("/question", async function (data, response) { + const { question } = data.body; + const questionId = Math.random().toString(16).slice(2, 8); + response.contentType = "application/json"; + if (typeof question != "string") { + response.response = { + status: "error", + message: "questionId is required" + }; + return; + } + console.log("Received question", question); + const polls = deepSeekPolls.value; + coursePolls.value = []; + const pollData = { + questionID: questionId, + question: question, + flag: true + }; + polls.forEach((poll) => { + poll.resolve(pollData); + }); + deepSeekPolls.cache = pollData; + response.response = { + status: "success", + data: { + questionId: questionId + } + }; +}); diff --git a/src/app/routers/test.ts b/src/app/routers/test.ts new file mode 100644 index 0000000..6c5ec6b --- /dev/null +++ b/src/app/routers/test.ts @@ -0,0 +1,31 @@ +import router from "../router"; +import { coursePolls, deepSeekPolls } from "../data/polls"; + +router.on("/test", async function (data, response) { + const question = "What is your name?"; + const questionId = Math.random().toString(16).slice(2, 8); + response.contentType = "application/json"; + console.log("Test question", question); + const polls = deepSeekPolls.value; + coursePolls.value = []; + if (polls.length == 0) { + deepSeekPolls.cache = { + questionID: questionId, + question: question, + flag: true + }; + } + polls.forEach((poll) => { + poll.resolve({ + questionID: questionId, + question: question, + flag: true + }); + }); + response.response = { + status: "success", + data: { + questionId: questionId + } + }; +}); diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000..aceeace --- /dev/null +++ b/src/const.ts @@ -0,0 +1,5 @@ +const CONST = { + pollInterval: 10000 +}; + +export default CONST; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9de5030 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +import * as http from "http"; +import app from "./app/app"; + +const server = http.createServer(); + +server.on("request", app.httpHandler()); + +server.listen(3000, () => { + console.log("VCLight serve"); + console.log("> Ready! Available at http://localhost:3000"); +}); diff --git a/src/utils/poll.ts b/src/utils/poll.ts new file mode 100644 index 0000000..64f3fe8 --- /dev/null +++ b/src/utils/poll.ts @@ -0,0 +1,27 @@ +export default class Poll { + protected inner: Promise; + protected timer: Promise; + protected resolver: (value: T | PromiseLike) => void; + static TIMEOUT = class Timeout {}; + + constructor(protected timeout: number) { + let resolver: (value: T | PromiseLike) => void; + this.inner = new Promise((r) => { + resolver = r; + }); + this.resolver = resolver!; + this.timer = new Promise((resolve) => { + setTimeout(() => { + resolve(Poll.TIMEOUT); + }, timeout); + }); + } + + public resolve(value: T | PromiseLike) { + this.resolver(value); + } + + public async wait(): Promise { + return Promise.race([this.inner, this.timer]); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..546599f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2024", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +}