refactor: 后端从 Django 换成 Bun + SQLite
Django 多个 worker 内存占用 1-2 GB,换成 Bun 单进程后约 25 MB, 同时去掉 Postgres 和 Redis,只剩 Caddy + Bun 两个容器。 - server/:Hono + bun:sqlite + Bun 原生 WebSocket,接口与原 django-ninja 保持兼容;表结构、时间/UUID 存储格式、密码哈希与 Django 一致,旧数据可直接使用 - 评分加权、贝叶斯平滑、成绩册的计算结果与原实现逐位一致(复刻 Python round/sum) - scripts/:Postgres → SQLite 迁移、新建超管、提示词层级标注、重算分数 - 前端:Django Admin 去掉后,用户新建/修改改为页面内弹窗; 非超管的挑战公开按钮置灰 - 权限不足返回 JSON 403(原来是重定向到不存在的登录页) - docker-compose / Caddyfile / dev.sh / CLAUDE.md 更新,删除 AGENTS.md - 服务器切换步骤见 docs/migrate-to-bun.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47d4679984
commit
26f05d1d76
297 files changed
+4659
-42956
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { all, get, run } from "../db"
|
||||
import { complete, llmConfigured } from "./llm"
|
||||
|
||||
const CLASSIFY_SYSTEM_PROMPT = `你是一个教育评估专家。根据布鲁姆认知分类学,分析以下学生在前端学习中发送给AI助手的一条提示词,判断该提示词所体现的认知层级。
|
||||
|
||||
层级定义:
|
||||
- L1 记忆:能背诵HTML标签语法(例:"帮我写一个按钮")
|
||||
- L2 理解:能解释flex布局原理(例:"为什么这里不居中?")
|
||||
- L3 应用:能独立搭建页面结构(例:"用flex做导航栏,间距16px")
|
||||
- L4 分析:能定位跨浏览器兼容性bug(例:"Safari中margin失效,原因?")
|
||||
- L5 评价:能对比并选择方案(例:"对比Grid与Flex方案优劣")
|
||||
- L6 创造:能设计并实现原创交互作品(例:"设计夜间/日间切换效果")
|
||||
|
||||
只返回一个数字(1-6),不要解释。`
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
async function callLlm(content: string): Promise<number | null> {
|
||||
if (!llmConfigured()) return null
|
||||
try {
|
||||
const text = await complete(
|
||||
[
|
||||
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
||||
{ role: "user", content },
|
||||
],
|
||||
{ maxTokens: 10, timeoutMs: 30_000 },
|
||||
)
|
||||
const match = /[1-6]/.exec(text)
|
||||
if (!match) {
|
||||
console.warn(`classify: unexpected LLM response '${text}'`)
|
||||
return null
|
||||
}
|
||||
return Number(match[0])
|
||||
} catch (e) {
|
||||
console.error("classify LLM call failed:", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function classifyMessage(messageId: number): Promise<number | null> {
|
||||
const msg = get<{ content: string }>(
|
||||
`SELECT content FROM prompt_message WHERE id = $id AND role = 'user'`,
|
||||
{ id: messageId },
|
||||
)
|
||||
if (!msg) return null
|
||||
const level = await callLlm(msg.content)
|
||||
if (level !== null) {
|
||||
run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, {
|
||||
level,
|
||||
id: messageId,
|
||||
})
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
export async function classifyConversationMessages(conversationId: string, force = false) {
|
||||
const rows = all<{ id: number }>(
|
||||
`SELECT id FROM prompt_message
|
||||
WHERE conversation_id = $id AND role = 'user' ${force ? "" : "AND prompt_level IS NULL"}
|
||||
ORDER BY created, id`,
|
||||
{ id: conversationId },
|
||||
)
|
||||
for (const { id } of rows) {
|
||||
await classifyMessage(id)
|
||||
await sleep(300)
|
||||
}
|
||||
}
|
||||
|
||||
export async function classifyMessagesBatch(ids: number[]) {
|
||||
for (const id of ids) {
|
||||
await classifyMessage(id)
|
||||
await sleep(500)
|
||||
}
|
||||
}
|
||||
|
||||
// 后台执行,不阻塞请求
|
||||
export function inBackground(p: Promise<unknown>) {
|
||||
p.catch((e) => console.error("background task failed:", e))
|
||||
}
|
||||
Reference in new issue
Block a user