feat: 去掉 Judge0 依赖,SQL 改为浏览器内执行,C/C++ 改走后端 go-judge
Deploy / build-and-deploy (push) Canceled after 0s
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:
1 parent
0164e10513
commit
998e6bb5dd
12 files changed
+181
-83
No files matched your search
@@ -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_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39
|
||||||
PUBLIC_CODEAPI_URL=http://localhost:8080
|
PUBLIC_CODEAPI_URL=http://localhost:8080
|
||||||
@@ -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_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_CODEAPI_URL=https://code.xuyue.cc/api
|
||||||
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc
|
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc
|
||||||
@@ -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_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_CODEAPI_URL=http://10.13.114.114:82/api
|
||||||
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
|
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
|
||||||
Generated
+46
-3
@@ -24,6 +24,7 @@
|
|||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"skulpt": "^1.2.0",
|
"skulpt": "^1.2.0",
|
||||||
|
"sql.js": "^1.15.1",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-codemirror": "^6.1.1"
|
"vue-codemirror": "^6.1.1"
|
||||||
},
|
},
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"@rsbuild/core": "^2.0.5",
|
"@rsbuild/core": "^2.0.5",
|
||||||
"@rsbuild/plugin-vue": "^1.2.7",
|
"@rsbuild/plugin-vue": "^1.2.7",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/sql.js": "^1.4.11",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
@@ -220,9 +222,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -634,6 +636,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/file-saver": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
@@ -656,6 +665,27 @@
|
|||||||
"@types/lodash": "*"
|
"@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": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.21",
|
"version": "0.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
@@ -1539,6 +1569,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/style-mod": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
@@ -1586,6 +1622,13 @@
|
|||||||
"node": ">=14.17"
|
"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": {
|
"node_modules/vdirs": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"skulpt": "^1.2.0",
|
"skulpt": "^1.2.0",
|
||||||
|
"sql.js": "^1.15.1",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-codemirror": "^6.1.1"
|
"vue-codemirror": "^6.1.1"
|
||||||
},
|
},
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"@rsbuild/core": "^2.0.5",
|
"@rsbuild/core": "^2.0.5",
|
||||||
"@rsbuild/plugin-vue": "^1.2.7",
|
"@rsbuild/plugin-vue": "^1.2.7",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/sql.js": "^1.4.11",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
|
|||||||
+41
-35
@@ -1,21 +1,7 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { languageToId } from "./templates"
|
import type { SqlJob } from "./composables/sqlTable"
|
||||||
import { Code, LANGUAGE, Submission } from "./types"
|
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 })
|
const api = axios.create({ baseURL: import.meta.env.PUBLIC_CODEAPI_URL })
|
||||||
|
|
||||||
type PythonWorkerRequest = {
|
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) {
|
export async function submit(code: Code, input: string) {
|
||||||
if (code.language === "python") {
|
if (code.language === "python") {
|
||||||
return runPythonInWorker(code.value, input)
|
return runPythonInWorker(code.value, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodedCode = encode(code.value)
|
try {
|
||||||
|
const res = await api.post<{ status: number; output: string }>("/run", {
|
||||||
const id = languageToId[code.language]
|
code: code.value,
|
||||||
let compilerOptions = ""
|
language: code.language,
|
||||||
if (id === 50) compilerOptions = "-lm" // 解决 GCC 的链接问题
|
stdin: input,
|
||||||
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 },
|
|
||||||
})
|
})
|
||||||
const data = response.data
|
return res.data
|
||||||
|
} catch (err: any) {
|
||||||
return {
|
return {
|
||||||
status: data.status && data.status.id,
|
status: 11,
|
||||||
output: [decode(data.compile_output), decode(data.stdout)]
|
output: err?.response?.data?.detail ?? "运行服务不可用,请稍后再试",
|
||||||
.join("\n")
|
}
|
||||||
.trim(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { useStorage } from "@vueuse/core"
|
|||||||
import copyTextToClipboard from "copy-text-to-clipboard"
|
import copyTextToClipboard from "copy-text-to-clipboard"
|
||||||
import qs from "query-string"
|
import qs from "query-string"
|
||||||
import { nextTick, reactive, ref, watch } from "vue"
|
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 { sources } from "../templates"
|
||||||
import { Cache, Code, LANGUAGE, Status } from "../types"
|
import { Cache, Code, LANGUAGE, Status } from "../types"
|
||||||
import { atou, utoa } from "../utils"
|
import { atou, utoa } from "../utils"
|
||||||
import { isMobile } from "./breakpoints"
|
import { isMobile } from "./breakpoints"
|
||||||
import { dialog, notice } from "./notice"
|
import { dialog, notice } from "./notice"
|
||||||
import {
|
import {
|
||||||
buildSqlScript,
|
buildSqlJob,
|
||||||
resetSqlTableSelection,
|
resetSqlTableSelection,
|
||||||
selectSqlTable,
|
selectSqlTable,
|
||||||
selectedTableId,
|
selectedTableId,
|
||||||
@@ -177,10 +177,11 @@ export async function run() {
|
|||||||
} else {
|
} else {
|
||||||
output.value = ""
|
output.value = ""
|
||||||
status.value = Status.NotStarted
|
status.value = Status.NotStarted
|
||||||
const sourceCode =
|
const result =
|
||||||
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
|
code.language === "sql"
|
||||||
const result = await submit(
|
? await runSql(buildSqlJob(cleanCode))
|
||||||
{ value: sourceCode, language: code.language },
|
: await submit(
|
||||||
|
{ value: cleanCode, language: code.language },
|
||||||
input.value.trim(),
|
input.value.trim(),
|
||||||
)
|
)
|
||||||
output.value = result.output || ""
|
output.value = result.output || ""
|
||||||
|
|||||||
+12
-13
@@ -19,21 +19,20 @@ function isQuery(sql: string): boolean {
|
|||||||
return /^\s*(SELECT|WITH)\b/i.test(sql)
|
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 =
|
const table =
|
||||||
sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables[0]
|
sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables[0]
|
||||||
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
|
return {
|
||||||
if (isQuery(studentSql.trim())) {
|
setup: buildSetupSql(table),
|
||||||
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
|
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 {
|
export interface SqlResult {
|
||||||
@@ -41,7 +40,7 @@ export interface SqlResult {
|
|||||||
rows: Record<string, string | number>[]
|
rows: Record<string, string | number>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出为 sqlite CLI 的 list 模式(| 分隔),开启 .headers on 后首行是列名
|
// 输出为 sqlite CLI 的 list 模式(| 分隔),首行是列名
|
||||||
export function parseResult(output: string): SqlResult {
|
export function parseResult(output: string): SqlResult {
|
||||||
const lines = output
|
const lines = output
|
||||||
.split("\n")
|
.split("\n")
|
||||||
|
|||||||
Vendored
-1
@@ -1,5 +1,4 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly PUBLIC_JUDGE0API_URL: string
|
|
||||||
readonly PUBLIC_MAXKB_URL: string
|
readonly PUBLIC_MAXKB_URL: string
|
||||||
readonly PUBLIC_CODEAPI_URL: string
|
readonly PUBLIC_CODEAPI_URL: string
|
||||||
readonly PUBLIC_ICONIFY_URL: string
|
readonly PUBLIC_ICONIFY_URL: string
|
||||||
|
|||||||
@@ -19,14 +19,6 @@ turtle.done()`
|
|||||||
|
|
||||||
const sqlSource = ""
|
const sqlSource = ""
|
||||||
|
|
||||||
export const languageToId: { [key in string]: number } = {
|
|
||||||
c: 50,
|
|
||||||
cpp: 54,
|
|
||||||
java: 62,
|
|
||||||
python: 71,
|
|
||||||
sql: 82,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sources = {
|
export const sources = {
|
||||||
c: cSource,
|
c: cSource,
|
||||||
cpp: cppSource,
|
cpp: cppSource,
|
||||||
|
|||||||
@@ -21,20 +21,6 @@ export enum Status {
|
|||||||
RuntimeError = 11,
|
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 {
|
export interface File {
|
||||||
in: string
|
in: string
|
||||||
out: string
|
out: string
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user