服务端 - 删掉没人用的 /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>
154 lines
4.0 KiB
TypeScript
154 lines
4.0 KiB
TypeScript
import { ref } from "vue"
|
|
import { ReconnectingSocket } from "../utils/socket"
|
|
import { html, css, js } from "./editors"
|
|
|
|
export interface PromptMessage {
|
|
role: "user" | "assistant"
|
|
content: string
|
|
id?: number // assistant message backend pk (for deletion)
|
|
code?: { html: string | null; css: string | null; js: string | null }
|
|
submitted?: boolean // whether this assistant message's code has been submitted
|
|
created?: string
|
|
}
|
|
|
|
export const messages = ref<PromptMessage[]>([])
|
|
export const conversationId = ref<string>("")
|
|
export const connected = ref(false)
|
|
export const streaming = ref(false)
|
|
export const streamingContent = ref("")
|
|
export const currentTaskId = ref(0)
|
|
let _onCodeComplete:
|
|
| ((
|
|
code: { html: string | null; css: string | null; js: string | null },
|
|
messageId: number
|
|
) => void)
|
|
| null = null
|
|
|
|
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
|
|
_onCodeComplete = fn
|
|
}
|
|
|
|
function onMessage(data: any) {
|
|
if (data.type === "init") {
|
|
streaming.value = false
|
|
streamingContent.value = ""
|
|
const alreadyLoaded = conversationId.value === data.conversation_id
|
|
conversationId.value = data.conversation_id
|
|
if (!alreadyLoaded) {
|
|
messages.value = data.messages || []
|
|
const lastAssistant = [...messages.value]
|
|
.reverse()
|
|
.find((m) => m.role === "assistant" && m.code)
|
|
if (lastAssistant?.code) {
|
|
applyCode(lastAssistant.code)
|
|
}
|
|
}
|
|
} else if (data.type === "stream") {
|
|
streaming.value = true
|
|
streamingContent.value += data.content
|
|
} else if (data.type === "complete") {
|
|
streaming.value = false
|
|
messages.value.push({
|
|
role: "assistant",
|
|
content: streamingContent.value,
|
|
id: data.message_id,
|
|
code: data.code,
|
|
submitted: false,
|
|
})
|
|
streamingContent.value = ""
|
|
if (data.code) {
|
|
applyCode(data.code)
|
|
if (_onCodeComplete) {
|
|
_onCodeComplete(data.code, data.message_id)
|
|
}
|
|
}
|
|
} else if (data.type === "error") {
|
|
streaming.value = false
|
|
streamingContent.value = ""
|
|
messages.value.push({
|
|
role: "assistant",
|
|
content: data.content,
|
|
})
|
|
}
|
|
}
|
|
|
|
const socket = new ReconnectingSocket({
|
|
onOpen() {
|
|
connected.value = true
|
|
},
|
|
onMessage,
|
|
onDrop() {
|
|
connected.value = false
|
|
// 断开时服务端会删掉还没有回复的提问,这边也去掉
|
|
if (streaming.value) dropPendingPrompt()
|
|
},
|
|
onGiveUp() {
|
|
messages.value.push({
|
|
role: "assistant",
|
|
content: "与服务器的连接已断开,请刷新页面重试",
|
|
})
|
|
},
|
|
})
|
|
|
|
export function connectPrompt(taskId: number) {
|
|
currentTaskId.value = taskId
|
|
socket.connect(`/ws/prompt/${taskId}/`)
|
|
}
|
|
|
|
export function disconnectPrompt() {
|
|
socket.close()
|
|
messages.value = []
|
|
conversationId.value = ""
|
|
connected.value = false
|
|
streaming.value = false
|
|
streamingContent.value = ""
|
|
currentTaskId.value = 0
|
|
_onCodeComplete = null
|
|
}
|
|
|
|
export function sendPrompt(content: string, model: string = "") {
|
|
if (!socket.send({ type: "message", content, model })) return
|
|
streaming.value = true
|
|
messages.value.push({ role: "user", content })
|
|
}
|
|
|
|
function dropPendingPrompt() {
|
|
if (
|
|
messages.value.length > 0 &&
|
|
messages.value[messages.value.length - 1].role === "user"
|
|
) {
|
|
messages.value.pop()
|
|
}
|
|
streaming.value = false
|
|
streamingContent.value = ""
|
|
}
|
|
|
|
export function stopPrompt() {
|
|
dropPendingPrompt()
|
|
if (currentTaskId.value) {
|
|
connectPrompt(currentTaskId.value)
|
|
}
|
|
}
|
|
|
|
export function markMessageSubmitted(assistantMsgId: number) {
|
|
const msg = messages.value.find((m) => m.id === assistantMsgId)
|
|
if (msg) msg.submitted = true
|
|
}
|
|
|
|
export function removeMessagePair(assistantMsgId: number) {
|
|
const idx = messages.value.findIndex((m) => m.id === assistantMsgId)
|
|
if (idx >= 1) {
|
|
messages.value.splice(idx - 1, 2)
|
|
}
|
|
}
|
|
|
|
function applyCode(code: {
|
|
html: string | null
|
|
css: string | null
|
|
js: string | null
|
|
}) {
|
|
if (code.html !== null) html.value = code.html
|
|
if (code.css !== null) css.value = code.css
|
|
if (code.js !== null) js.value = code.js
|
|
}
|