Initial Commit

This commit is contained in:
Bluemangoo 2025-05-08 20:05:49 +08:00
commit 2d575e2e84
Signed by: Bluemangoo
GPG Key ID: F2F7E46880A1C4CF
19 changed files with 1141 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Project Configs
.idea
# Node Modules
node_modules
# Cache

5
.prettierrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 4
}

21
package.json Normal file
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
pnpm-lock.yaml generated Normal file
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/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: {}

146
script/buct-cource-copy.js Normal file
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
script/ds-listener.js Normal file
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
script/tempermonkey.d.ts vendored Normal file
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'}texthtml
*/
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
src/app/app.ts Normal file
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
src/app/data/polls.ts Normal file
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
src/app/initRouter.ts Normal file
View File

@ -0,0 +1,4 @@
import "./routers/answer";
import "./routers/polling";
import "./routers/question";
import "./routers/test";

4
src/app/router.ts Normal file
View File

@ -0,0 +1,4 @@
import VCLightRouter from "@vclight/router";
const router = new VCLightRouter();
export default router;

27
src/app/routers/answer.ts Normal file
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;
});

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);
}
});

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
src/app/routers/test.ts Normal file
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
src/const.ts Normal file
View File

@ -0,0 +1,5 @@
const CONST = {
pollInterval: 10000
};
export default CONST;

11
src/main.ts Normal file
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
src/utils/poll.ts Normal file
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
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2024",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}