feat: 去掉 Judge0 依赖,SQL 改为浏览器内执行,C/C++ 改走后端 go-judge
Deploy / build-and-deploy (push) Canceled after 0s

- SQL 用 sql.js 在 Web Worker 中执行,带 5 秒超时
- C/C++ 改为请求后端 /run,请求失败时显示错误而不是一直卡在运行中
- 删除 Judge0 相关代码和 PUBLIC_JUDGE0API_URL 配置

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 16:27:02 +08:00
1 parent 0164e10513
commit 998e6bb5dd
12 files changed
+181 -83

No files matched your search

-1
View File
@@ -1,3 +1,2 @@
PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39
PUBLIC_CODEAPI_URL=http://localhost:8080
-1
View File
@@ -1,4 +1,3 @@
PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39
PUBLIC_CODEAPI_URL=https://code.xuyue.cc/api
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc
-1
View File
@@ -1,4 +1,3 @@
PUBLIC_JUDGE0API_URL=http://10.13.114.114:8082
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_CODEAPI_URL=http://10.13.114.114:82/api
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
+46 -3
View File
@@ -24,6 +24,7 @@
"normalize.css": "^8.0.1",
"query-string": "^9.3.1",
"skulpt": "^1.2.0",
"sql.js": "^1.15.1",
"vue": "^3.5.34",
"vue-codemirror": "^6.1.1"
},
@@ -32,6 +33,7 @@
"@rsbuild/core": "^2.0.5",
"@rsbuild/plugin-vue": "^1.2.7",
"@types/file-saver": "^2.0.7",
"@types/sql.js": "^1.4.11",
"core-js": "^3.49.0",
"prettier": "^3.8.3",
"typescript": "^6.0.3"
@@ -220,9 +222,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -634,6 +636,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/emscripten": {
"version": "1.41.6",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.6.tgz",
"integrity": "sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/file-saver": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
@@ -656,6 +665,27 @@
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.9.0"
}
},
"node_modules/@types/sql.js": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.11.tgz",
"integrity": "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/emscripten": "*",
"@types/node": "*"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@@ -1539,6 +1569,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sql.js": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.15.1.tgz",
"integrity": "sha512-i0M/6s+lcl67Flm/1m8/AeVU5caDramlILXU/udUwpTQym2qKMSe2sJiXB5d5WXFB272aj4B2v1JbtQ93ID3Pg==",
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
@@ -1586,6 +1622,13 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true,
"license": "MIT"
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
+2
View File
@@ -26,6 +26,7 @@
"normalize.css": "^8.0.1",
"query-string": "^9.3.1",
"skulpt": "^1.2.0",
"sql.js": "^1.15.1",
"vue": "^3.5.34",
"vue-codemirror": "^6.1.1"
},
@@ -34,6 +35,7 @@
"@rsbuild/core": "^2.0.5",
"@rsbuild/plugin-vue": "^1.2.7",
"@types/file-saver": "^2.0.7",
"@types/sql.js": "^1.4.11",
"core-js": "^3.49.0",
"prettier": "^3.8.3",
"typescript": "^6.0.3"
+41 -35
View File
@@ -1,21 +1,7 @@
import axios from "axios"
import { languageToId } from "./templates"
import { Code, LANGUAGE, Submission } from "./types"
import type { SqlJob } from "./composables/sqlTable"
import { Code, LANGUAGE } from "./types"
function encode(string?: string) {
return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
}
function decode(bytes?: string) {
const latin = atob(bytes ?? "")
return new TextDecoder("utf-8").decode(
Uint8Array.from({ length: latin.length }, (_, index) =>
latin.charCodeAt(index),
),
)
}
const judge = axios.create({ baseURL: import.meta.env.PUBLIC_JUDGE0API_URL })
const api = axios.create({ baseURL: import.meta.env.PUBLIC_CODEAPI_URL })
type PythonWorkerRequest = {
@@ -78,32 +64,52 @@ async function runPythonInWorker(source: string, stdin: string) {
})
}
// SQL 在浏览器里用 sql.js 执行;每次新开一个 worker,跑完或超时就销毁
export function runSql(job: SqlJob) {
return new Promise<{ status: number; output: string }>((resolve) => {
const worker = new Worker(
new URL("./workers/sqlite.worker.ts", import.meta.url),
{ type: "module" },
)
let timeout = 0
const finish = (result: { status: number; output: string }) => {
clearTimeout(timeout)
worker.terminate()
resolve(result)
}
worker.onerror = () => finish({ status: 11, output: "SQL 引擎加载失败" })
worker.onmessage = (event) => {
// 首次加载 wasm 不计入超时,引擎就绪后再提交并开始计时
if (event.data?.ready) {
timeout = window.setTimeout(
() => finish({ status: 11, output: "运行超时" }),
5000,
)
worker.postMessage(job)
return
}
finish(event.data)
}
})
}
export async function submit(code: Code, input: string) {
if (code.language === "python") {
return runPythonInWorker(code.value, input)
}
const encodedCode = encode(code.value)
const id = languageToId[code.language]
let compilerOptions = ""
if (id === 50) compilerOptions = "-lm" // 解决 GCC 的链接问题
const payload = {
source_code: encodedCode,
language_id: id,
stdin: encode(input),
redirect_stderr_to_stdout: true,
compiler_options: compilerOptions,
}
const response = await judge.post<Submission>("/submissions", payload, {
params: { base64_encoded: true, wait: true },
try {
const res = await api.post<{ status: number; output: string }>("/run", {
code: code.value,
language: code.language,
stdin: input,
})
const data = response.data
return res.data
} catch (err: any) {
return {
status: data.status && data.status.id,
output: [decode(data.compile_output), decode(data.stdout)]
.join("\n")
.trim(),
status: 11,
output: err?.response?.data?.detail ?? "运行服务不可用,请稍后再试",
}
}
}
+7 -6
View File
@@ -2,14 +2,14 @@ import { useStorage } from "@vueuse/core"
import copyTextToClipboard from "copy-text-to-clipboard"
import qs from "query-string"
import { nextTick, reactive, ref, watch } from "vue"
import { formatCode, getCodeByQuery, submit } from "../api"
import { formatCode, getCodeByQuery, runSql, submit } from "../api"
import { sources } from "../templates"
import { Cache, Code, LANGUAGE, Status } from "../types"
import { atou, utoa } from "../utils"
import { isMobile } from "./breakpoints"
import { dialog, notice } from "./notice"
import {
buildSqlScript,
buildSqlJob,
resetSqlTableSelection,
selectSqlTable,
selectedTableId,
@@ -177,10 +177,11 @@ export async function run() {
} else {
output.value = ""
status.value = Status.NotStarted
const sourceCode =
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
const result = await submit(
{ value: sourceCode, language: code.language },
const result =
code.language === "sql"
? await runSql(buildSqlJob(cleanCode))
: await submit(
{ value: cleanCode, language: code.language },
input.value.trim(),
)
output.value = result.output || ""
+12 -13
View File
@@ -19,21 +19,20 @@ function isQuery(sql: string): boolean {
return /^\s*(SELECT|WITH)\b/i.test(sql)
}
export function buildSqlScript(studentSql: string) {
export interface SqlJob {
setup: string
sql: string
echoTable: string | null
}
export function buildSqlJob(studentSql: string): SqlJob {
const table =
sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables[0]
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
if (isQuery(studentSql.trim())) {
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
return {
setup: buildSetupSql(table),
sql: studentSql,
echoTable: isQuery(studentSql.trim()) ? null : table.tableName,
}
return [
buildSetupSql(table),
".output /dev/null",
normalizedSql,
".output stdout",
".headers on",
`SELECT * FROM ${table.tableName};`,
].join("\n\n")
}
export interface SqlResult {
@@ -41,7 +40,7 @@ export interface SqlResult {
rows: Record<string, string | number>[]
}
// 输出为 sqlite CLI 的 list 模式(| 分隔),开启 .headers on 后首行是列名
// 输出为 sqlite CLI 的 list 模式(| 分隔),首行是列名
export function parseResult(output: string): SqlResult {
const lines = output
.split("\n")
-1
View File
@@ -1,5 +1,4 @@
interface ImportMetaEnv {
readonly PUBLIC_JUDGE0API_URL: string
readonly PUBLIC_MAXKB_URL: string
readonly PUBLIC_CODEAPI_URL: string
readonly PUBLIC_ICONIFY_URL: string
-8
View File
@@ -19,14 +19,6 @@ turtle.done()`
const sqlSource = ""
export const languageToId: { [key in string]: number } = {
c: 50,
cpp: 54,
java: 62,
python: 71,
sql: 82,
}
export const sources = {
c: cSource,
cpp: cppSource,
-14
View File
@@ -21,20 +21,6 @@ export enum Status {
RuntimeError = 11,
}
export interface Submission {
stdout: string
time: string
memory: number
stderr: string
token: string
compile_output: string
message: string
status: {
id: number
description: string
}
}
export interface File {
in: string
out: string
+73
View File
@@ -0,0 +1,73 @@
/// <reference lib="webworker" />
import initSqlJs, { type Database } from "sql.js"
type RunRequest = {
setup: string
sql: string
// 增删改语句执行后回显的表名;查询语句为 null,直接展示查询结果
echoTable: string | null
}
type RunResponse = {
status: number
output: string
}
// 取最后一条有结果列的语句(没有数据行时也保留列名)
function lastResult(db: Database, sql: string) {
let result: { columns: string[]; rows: unknown[][] } | null = null
for (const stmt of db.iterateStatements(sql)) {
const columns = stmt.getColumnNames()
if (columns.length === 0) {
stmt.step()
continue
}
const rows: unknown[][] = []
while (stmt.step()) rows.push(stmt.get())
result = { columns, rows }
}
return result
}
// 与 sqlite CLI 的 list 模式 + .headers on 一致:首行列名,| 分隔
function format(result: { columns: string[]; rows: unknown[][] } | null) {
if (!result) return ""
return [result.columns, ...result.rows]
.map((row) => row.map((value) => value ?? "").join("|"))
.join("\n")
}
const wasmUrl = new URL("sql.js/dist/sql-wasm-browser.wasm", import.meta.url)
.href
const ready = initSqlJs({ locateFile: () => wasmUrl })
ready.then(
() => (self as any).postMessage({ ready: true }),
() =>
(self as any).postMessage({
status: 11,
output: "SQL 引擎加载失败",
} satisfies RunResponse),
)
self.onmessage = async (event: MessageEvent<RunRequest>) => {
const { setup, sql, echoTable } = event.data
const SQL = await ready
const db = new SQL.Database()
try {
db.run(setup)
let result = lastResult(db, sql)
if (echoTable) result = lastResult(db, `SELECT * FROM ${echoTable};`)
;(self as any).postMessage({
status: 3,
output: format(result),
} satisfies RunResponse)
} catch (err: any) {
;(self as any).postMessage({
status: 11,
output: String(err?.message ?? err),
} satisfies RunResponse)
} finally {
db.close()
}
}