From cdeb7c18efcc585a89941b55e69a566b3bfc94a2 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 26 Oct 2026 21:33:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=AD=E5=8D=B1?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=9A=E6=B3=A8=E5=86=8C=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E3=80=81=E6=A8=A1=E5=9E=8B=E5=90=8D=E3=80=81LLM=20=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E3=80=81WebSocket=20=E6=96=AD=E7=BA=BF=E3=80=81?= =?UTF-8?q?=E6=9C=AA=E5=85=AC=E5=BC=80=E4=BB=BB=E5=8A=A1=E6=B3=84=E9=9C=B2?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 服务端 - 删掉没人用的 /account/register,外网不能再批量注册小号 - 模型名只放行豆包和配置的 LLM_MODEL,浏览器里存的旧模型名换成默认模型; 聊天默认模型改读 LLM_MODEL - 流式生成改成 120 秒无数据才超时(另有 15 分钟总上限),思考模型不会被中途掐断; SSE 最后一行没有换行时不再丢内容 - WebSocket:token 无效 401、任务不存在 404,建对话失败时关闭连接; 引导出错时把没有回复的提问移出上下文 - 未公开的教程只有超管能看,未公开的挑战和素材管理员也能看,其他人 404 - Caddy:/media 加 CSP sandbox 和 nosniff,上传的 html/svg 直接打开不能跑脚本; 加 CORS,预览 iframe 里的代码可以读取字体和 fetch 素材 前端 - 新增 ReconnectingSocket:意外断开后退避重连,握手失败先刷新 token; 断线时清掉“生成中”状态,旧连接的事件不再改写新连接 - marked 扩展抽到 utils/markdown.ts:教程用独立实例,不再改全局 marked; 学生内容的渲染保留高亮,不带会渲染原始 HTML 的 preview 扩展 - 挑战页、创作页加载完成前离开时不再建立连接,离开时关闭引导弹窗 - 教程、提交列表、用户管理跟随浏览器前进后退;快速翻页只用最后一次结果, 改筛选条件不再请求两次 - 随机评分弹窗在 AI 回复完之后不再弹出 Co-Authored-By: Claude Opus 5.5 --- Caddyfile | 8 + client/src/components/ai/PromptPanel.vue | 4 + .../src/components/ai/RandomRatingModal.vue | 2 + client/src/components/task/TaskPanel.vue | 6 +- .../src/components/task/TutorialContent.vue | 15 +- client/src/main.ts | 34 +---- client/src/pages/ChallengeDetail.vue | 12 +- client/src/pages/CreationDetail.vue | 11 +- client/src/pages/Submissions.vue | 31 +++- client/src/pages/UserManage.vue | 21 ++- client/src/store/guidance.ts | 82 +++++----- client/src/store/prompt.ts | 143 +++++++++--------- client/src/utils/markdown.ts | 37 ++++- client/src/utils/socket.ts | 80 ++++++++++ server/src/auth.ts | 4 + server/src/routes/account.ts | 13 -- server/src/routes/files.ts | 9 +- server/src/routes/tasks.ts | 6 +- server/src/services/llm.ts | 80 +++++++--- server/src/ws.ts | 24 ++- server/tests/api.test.ts | 28 ++++ server/tests/unit.test.ts | 49 +++++- 22 files changed, 489 insertions(+), 210 deletions(-) create mode 100644 client/src/utils/socket.ts diff --git a/Caddyfile b/Caddyfile index 9fc5739..89e2f67 100644 --- a/Caddyfile +++ b/Caddyfile @@ -8,6 +8,14 @@ } handle_path /media/* { + # 上传的 html/svg 直接打开时不能当成本站页面跑脚本(会读到 localStorage 里的 token); + # 作为图片、样式、脚本引用时不受 sandbox 影响。预览 iframe 没有 allow-same-origin, + # 读字体或 fetch() 素材要跨域许可 + header { + Content-Security-Policy "sandbox" + X-Content-Type-Options "nosniff" + Access-Control-Allow-Origin "*" + } root * /media file_server } diff --git a/client/src/components/ai/PromptPanel.vue b/client/src/components/ai/PromptPanel.vue index 237b43d..a6de48e 100644 --- a/client/src/components/ai/PromptPanel.vue +++ b/client/src/components/ai/PromptPanel.vue @@ -150,6 +150,10 @@ const modelOptions = [ { label: "DeepSeek", value: "deepseek-flash" }, ] const selectedModel = useStorage("prompt-model", "deepseek-flash") +// 浏览器里可能还存着改版前的模型名 +if (!modelOptions.some((o) => o.value === selectedModel.value)) { + selectedModel.value = "deepseek-flash" +} // Group messages into user+assistant pairs const submittingId = ref(null) diff --git a/client/src/components/ai/RandomRatingModal.vue b/client/src/components/ai/RandomRatingModal.vue index f10a1be..7db8298 100644 --- a/client/src/components/ai/RandomRatingModal.vue +++ b/client/src/components/ai/RandomRatingModal.vue @@ -65,6 +65,8 @@ function clearReshowTimer() { async function fetchAndShow() { try { const item = await Submission.randomForRating(current.value?.submission_id) + // 请求期间 AI 已经回复完了,就不要再弹出来 + if (!streaming.value) return if (item) { current.value = item previewContent.value = buildPreviewDocument({ diff --git a/client/src/components/task/TaskPanel.vue b/client/src/components/task/TaskPanel.vue index 142b4f5..ca9adce 100644 --- a/client/src/components/task/TaskPanel.vue +++ b/client/src/components/task/TaskPanel.vue @@ -170,7 +170,11 @@ function syncRoute(routeName: string) { } } syncRoute(route.name as string) -watch(() => route.name as string, syncRoute) +// 只换了 display(比如浏览器后退到上一节教程)也要同步 +watch( + () => [route.name as string, route.params.display] as const, + ([name]) => syncRoute(name), +) defineEmits(["hide"]) diff --git a/client/src/components/task/TutorialContent.vue b/client/src/components/task/TutorialContent.vue index 68a45b4..d181393 100644 --- a/client/src/components/task/TutorialContent.vue +++ b/client/src/components/task/TutorialContent.vue @@ -3,7 +3,8 @@