fix: 修复中危问题:注册接口、模型名、LLM 超时、WebSocket 断线、未公开任务泄露等
服务端 - 删掉没人用的 /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 <noreply@anthropic.com>
This commit is contained in:
1 parent
73788c4595
commit
cdeb7c18ef
22 files changed
+489
-210
No files matched your search
@@ -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<number | null>(null)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, useTemplateRef, watch } from "vue"
|
||||
import { marked } from "marked"
|
||||
import { Marked } from "marked"
|
||||
import { trustedExtensions } from "../../utils/markdown"
|
||||
import copyFn from "copy-text-to-clipboard"
|
||||
import { css, html, js, tab } from "../../store/editors"
|
||||
import { Tutorial } from "../../api"
|
||||
@@ -11,7 +12,8 @@ import { step, tutorialIds, loadTutorials } from "../../store/tutorial"
|
||||
import { taskId, assetBaseUrl } from "../../store/task"
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
marked.use({
|
||||
// 独立实例:带“复制/替换”按钮的代码块只用在教程里,不能改全局的 marked
|
||||
const tutorialMarked = new Marked(...trustedExtensions, {
|
||||
renderer: {
|
||||
code({ text, lang }) {
|
||||
const language = lang?.toLowerCase() ?? "html"
|
||||
@@ -36,15 +38,22 @@ const router = useRouter()
|
||||
const content = ref("")
|
||||
const $content = useTemplateRef<any>("$content")
|
||||
|
||||
// 连点“下一节”时只用最后一次请求的结果
|
||||
let renderSeq = 0
|
||||
|
||||
async function render() {
|
||||
const seq = ++renderSeq
|
||||
const data = await Tutorial.get(step.value)
|
||||
if (seq !== renderSeq) return
|
||||
taskId.value = data.task_ptr
|
||||
assetBaseUrl.value = `/media/tasks/tutorial/${step.value}/`
|
||||
html.value = data.example_html ?? ""
|
||||
css.value = data.example_css ?? ""
|
||||
js.value = data.example_js ?? ""
|
||||
const merged = `# ${data.display}. ${data.title}\n${data.content}`
|
||||
content.value = await marked.parse(merged, { async: true })
|
||||
const rendered = await tutorialMarked.parse(merged, { async: true })
|
||||
if (seq !== renderSeq) return
|
||||
content.value = rendered
|
||||
}
|
||||
|
||||
function flash(btn: HTMLButtonElement, done: string, original: string) {
|
||||
|
||||
Reference in new issue
Block a user