Initial Commit

This commit is contained in:
2025-05-08 20:05:49 +08:00
commit 2d575e2e84
19 changed files with 1141 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# Project Configs
.idea
# Node Modules
node_modules
# Cache
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 4
}
+21
View File
@@ -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"
}
}
+240
View File
@@ -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/[email protected])([email protected])
typescript:
specifier: ^5.8.3
version: 5.8.3
packages:
'@cspotcode/[email protected]':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@jridgewell/[email protected]':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/[email protected]':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/[email protected]':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@tsconfig/[email protected]':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
'@tsconfig/[email protected]':
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
'@tsconfig/[email protected]':
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
'@tsconfig/[email protected]':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@types/[email protected]':
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
'@vclight/[email protected]':
resolution: {integrity: sha512-oLojdFEagVZtjomTLdTqg+MSV72YQAzoBxNsWr/fCCyPHxvZe2G5/lqc2KdDqIBpwCGkeMZP5n2AK03BnD51eg==}
[email protected]:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
[email protected]:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
[email protected]:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==}
engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
[email protected]:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
[email protected]:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
[email protected]:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
[email protected]:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
[email protected]:
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
engines: {node: '>=14'}
hasBin: true
[email protected]:
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
[email protected]:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
[email protected]:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
[email protected]:
resolution: {integrity: sha512-ZQ5pWzTo9iRblGL8WikUPT4O7MKl0hSSj/Tu/NMmFv1XCF04lfydXG0yDOoTcvLI5JvAawpABKH/anmhmTcjrw==}
[email protected]:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
[email protected]:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
snapshots:
'@cspotcode/[email protected]':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@jridgewell/[email protected]': {}
'@jridgewell/[email protected]': {}
'@jridgewell/[email protected]':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@tsconfig/[email protected]': {}
'@tsconfig/[email protected]': {}
'@tsconfig/[email protected]': {}
'@tsconfig/[email protected]': {}
'@types/[email protected]':
dependencies:
undici-types: 6.21.0
'@vclight/[email protected]':
dependencies:
cookie: 0.7.0
vclight: 3.3.2
[email protected]:
dependencies:
acorn: 8.14.1
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
once: 1.4.0
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
wrappy: 1.0.2
[email protected]: {}
[email protected](@types/[email protected])([email protected]):
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
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
content-type: 1.0.5
cookie: 0.7.0
end-of-stream: 1.4.4
etag: 1.8.1
[email protected]: {}
[email protected]: {}
+146
View File
@@ -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);
})();
+155
View File
@@ -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);
})();
+308
View File
@@ -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<CONTEXT_TYPE>(
details: GM_Types.XHRDetails<CONTEXT_TYPE>
): GM_Types.AbortHandle<void>;
/**
将给定的 URL 下载到本地磁盘。
*/
declare function GM_download(details: GM_Types.DownloadDetails): GM_Types.AbortHandle<boolean>;
/**
将给定的 URL 下载到本地磁盘。
*/
declare function GM_download(url: string, filename: string): GM_Types.AbortHandle<boolean>;
/**
只要此选项卡处于打开状态,就获取一个持久对象。
*/
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<CONTEXT_TYPE> 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<CONTEXT_TYPE> extends XHRResponse<CONTEXT_TYPE> {
done: number;
lengthComputable: boolean;
loaded: number;
position: number;
total: number;
totalSize: number;
}
type Listener<OBJ> = (this: OBJ, event: OBJ) => any;
interface XHRDetails<CONTEXT_TYPE> {
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<XHRResponse<CONTEXT_TYPE>>;
onloadstart?: Listener<XHRResponse<CONTEXT_TYPE>>;
onprogress?: Listener<XHRProgress<CONTEXT_TYPE>>;
onreadystatechange?: Listener<XHRResponse<CONTEXT_TYPE>>;
ontimeout?: Listener<Function>;
onabort?: Function;
onerror?: Function;
}
interface AbortHandle<RETURN_TYPE> {
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<DownloadError>;
ontimeout?: Listener<object>;
onload?: Listener<object>;
onprogress?: Listener<XHRProgress<void>>;
}
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;
}
}
+14
View File
@@ -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;
+45
View File
@@ -0,0 +1,45 @@
import Poll from "../../utils/poll";
import CONST from "../../const";
type Polls<T> = { value: Poll<T>[]; cache: T };
export const coursePolls: Polls<CoursePollData> = {
value: [],
cache: {
questionID: "",
answer: "",
flag: false
}
};
export const deepSeekPolls: Polls<DeepSeekPollData> = {
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<CoursePollData>(CONST.pollInterval);
coursePolls.value.push(poll);
return poll.wait();
}
export function pollDeepSeek() {
const poll = new Poll<DeepSeekPollData>(CONST.pollInterval);
deepSeekPolls.value.push(poll);
return poll.wait();
}
+4
View File
@@ -0,0 +1,4 @@
import "./routers/answer";
import "./routers/polling";
import "./routers/question";
import "./routers/test";
+4
View File
@@ -0,0 +1,4 @@
import VCLightRouter from "@vclight/router";
const router = new VCLightRouter();
export default router;
+27
View File
@@ -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;
});
+48
View File
@@ -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 = <CoursePollData | DeepSeekPollData>pollData;
pollData.flag = false;
response.response = {
status: "success",
data: pollData
};
console.log("Polling data", pollData);
}
});
+33
View File
@@ -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
}
};
});
+31
View File
@@ -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
}
};
});
+5
View File
@@ -0,0 +1,5 @@
const CONST = {
pollInterval: 10000
};
export default CONST;
+11
View File
@@ -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");
});
+27
View File
@@ -0,0 +1,27 @@
export default class Poll<T> {
protected inner: Promise<T>;
protected timer: Promise<typeof Poll.TIMEOUT>;
protected resolver: (value: T | PromiseLike<T>) => void;
static TIMEOUT = class Timeout {};
constructor(protected timeout: number) {
let resolver: (value: T | PromiseLike<T>) => void;
this.inner = new Promise<T>((r) => {
resolver = r;
});
this.resolver = resolver!;
this.timer = new Promise((resolve) => {
setTimeout(() => {
resolve(Poll.TIMEOUT);
}, timeout);
});
}
public resolve(value: T | PromiseLike<T>) {
this.resolver(value);
}
public async wait(): Promise<T | typeof Poll.TIMEOUT> {
return Promise.race([this.inner, this.timer]);
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2024",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}