试用前排查:WebSocket 心跳与断线重连、登录失败按账号限制、旧浏览器提示
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 服务器 30 秒发一次 ping:Nginx Proxy Manager 默认 60 秒没数据就断开,窗体开着不操作程序会被结束
- 连接意外断开时提示、清掉失效的窗体、自动重连,连上后提示重新运行
- 登录失败:同一账号 10 次只锁这个账号,同一 IP 300 次,登录成功不再清零 IP 计数
- index.html:浏览器太旧时说明原因和解决办法,加载慢时提示稍等

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 09:05:05 +08:00
1 parent 982b1297aa
commit 27fdc3615d
8 files changed
+137 -23

No files matched your search

+9 -1
View File
@@ -79,7 +79,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
:box="box"
@reply="session.replyMessageBox(box.requestId, $event)"
/>
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
<div v-if="state.connection === 'lost'" class="desktop-hint lost">与服务器的连接断开了,正在重新连接…</div>
<div v-else-if="state.connection === 'restored' && state.forms.length === 0" class="desktop-hint lost">
连接已恢复。刚才的程序随连接断开已经结束,请重新运行
</div>
<div v-else-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
<div class="desktop-tools">
<span v-if="scale < 1" class="desktop-scale" title="窗体比运行区大,按比例缩小显示;点&quot;放大&quot;可以看得更清楚">
缩放 {{ Math.round(scale * 100) }}%
@@ -219,6 +223,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
justify-content: center;
color: rgb(255 255 255 / 0.7);
}
.desktop-hint.lost {
color: #ffd591;
font-weight: 600;
}
.photos {
font-size: 12px;
+34 -1
View File
@@ -45,6 +45,11 @@ export function createRunSession() {
/** 正在播放的声音(媒体播放器控件,实训台上的广播喇叭随之亮起) */
playing: null as { name: string } | null,
metrics: { compileMs: 0, startMs: 0, lastRoundTripMs: 0 },
/**
* 和服务器的连接:lost 表示意外断开(网络不稳、服务器在更新),正在自动重连;
* restored 表示断开后又连上了(提示学生刚才的程序已经结束),下次运行时变回 open
*/
connection: 'connecting' as 'connecting' | 'open' | 'lost' | 'restored',
})
/** id → 控件节点,patch 按 id 修改节点属性 */
const nodes = shallowReactive(new Map<string, ControlNode>())
@@ -54,6 +59,12 @@ export function createRunSession() {
/** 正在建立或已建立的连接;同时多处调用 connect() 时共用同一个 */
let connecting: Promise<void> | undefined
let actionSentAt = 0
/** 离开页面时主动断开,不再重连 */
let closed = false
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
/** 连续重连失败的次数,等待时间随之加长(最长 RECONNECT_MAX_MS) */
let reconnectFailures = 0
const RECONNECT_MAX_MS = 10_000
/** 每个媒体播放器控件一个 <audio> */
const players = new Map<string, HTMLAudioElement>()
@@ -92,12 +103,27 @@ export function createRunSession() {
const ws = new WebSocket(`${protocol}://${location.host}/ws?token=${encodeURIComponent(token ?? '')}`)
socket = ws
ws.onmessage = (event) => handle(JSON.parse(event.data) as ServerMessage)
ws.onopen = () => resolve()
ws.onopen = () => {
state.connection = state.connection === 'lost' ? 'restored' : 'open'
reconnectFailures = 0
resolve()
}
ws.onclose = () => {
// 服务器那边的程序随连接一起结束了;窗体留在页面上只会"点了没反应"
if (!closed && (state.forms.length > 0 || state.status === 'compiling' || state.status === 'running')) {
state.errors.push('与服务器的连接断开了(网络不稳或服务器正在更新),程序已经结束。连上之后请重新运行。')
}
stopAudio()
state.forms = []
state.messageBoxes = []
state.status = 'stopped'
state.board = null
socket = undefined
connecting = undefined
if (!closed) {
state.connection = 'lost'
reconnectTimer = setTimeout(() => void connect(), Math.min(1000 * 2 ** reconnectFailures++, RECONNECT_MAX_MS))
}
resolve()
}
}),
@@ -112,6 +138,11 @@ export function createRunSession() {
/** sampleId:当前实验(服务器据此在第一次运行时建好实验预置的数据库) */
async function run(files: SourceFile[], sampleId?: string) {
await connect()
if (socket?.readyState !== WebSocket.OPEN) {
state.errors = ['连不上服务器,请检查网络,稍后再点"运行"。']
return
}
state.connection = 'open'
state.diagnostics = []
state.console = ''
state.errors = []
@@ -251,6 +282,8 @@ export function createRunSession() {
/** 离开页面时断开连接:服务器随即结束正在运行的程序 */
function close() {
closed = true
clearTimeout(reconnectTimer)
socket?.close()
}