diff --git a/CLAUDE.md b/CLAUDE.md index b3b7e6a..914b72c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - `src/auth.ts` — Django-compatible password hashing; JWT login, no cookies: `POST /account/login` returns a 15-min HS256 `access_token` (sent as `Authorization: Bearer`, signed with `JWT_SECRET`, random per process if unset) and a 14-day opaque `refresh_token` (only its sha256 is stored, in `auth_refresh_token`; `POST /account/refresh` trades it for a new access token, logout / password change / deactivation delete it). An invalid or expired Bearer token is a 401 even on public endpoints so the client knows to refresh. `requireUser/requireAdmin/requireSuper` (401 / 403) - `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `prompt` - `src/services/` — `score.ts` (rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones), `gradebook.ts`, `llm.ts` (OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background), `cascade.ts` (Django's CASCADE deletes, done by hand), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit) - - `src/ws.ts` — `/ws/prompt//?token=` (code generation chat) and `/ws/guidance//?token=...` (prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request aborted + - `src/ws.ts` — `/ws/prompt//?token=` (code generation chat) and `/ws/guidance//?token=...` (prompt coaching); one message per connection at a time (a message sent while the AI is still replying gets an error, payloads capped at 64 KB); on disconnect the pending user message is deleted and the LLM request aborted - `scripts/` — one-off CLIs: `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts` - Roles: `super`, `admin`, `normal`. Task model is multi-table: `task_task` + one of `task_tutorial` / `task_challenge` / `task_creation` (`task_ptr_id`) diff --git a/client/src/components/Login.vue b/client/src/components/Login.vue index 79e57ec..aa970c2 100644 --- a/client/src/components/Login.vue +++ b/client/src/components/Login.vue @@ -48,9 +48,9 @@ /> (null) const selectedUsername = ref(null) const studentPassword = ref("") const studentLoading = ref(false) -const showStudentError = ref(false) +const studentError = ref("") // Classes data const classes = ref([]) @@ -135,7 +135,7 @@ const nameOptions = computed(() => const adminName = ref("") const adminPassword = ref("") const adminLoading = ref(false) -const showAdminError = ref(false) +const adminError = ref("") // Load classes on mount (cached — not reloaded on tab switch) onMounted(async () => { @@ -155,13 +155,13 @@ function onTabChange() { selectedClass.value = null selectedUsername.value = null studentPassword.value = "" - showStudentError.value = false + studentError.value = "" namesError.value = false names.value = [] // Reset admin tab state adminName.value = "" adminPassword.value = "" - showAdminError.value = false + adminError.value = "" } async function onClassChange(classname: string) { @@ -178,10 +178,15 @@ async function onClassChange(classname: string) { } } +// 失败次数太多被暂时禁止登录时,显示服务端给的提示 +function loginError(err: any, fallback: string) { + return err.response?.status === 429 ? err.response.data.detail : fallback +} + async function submitStudent() { if (!selectedUsername.value || !studentPassword.value) return studentLoading.value = true - showStudentError.value = false + studentError.value = "" try { const data = await Account.login( selectedUsername.value, @@ -194,8 +199,8 @@ async function submitStudent() { router.replace( window.location.pathname + window.location.search + window.location.hash, ) - } catch { - showStudentError.value = true + } catch (err: any) { + studentError.value = loginError(err, "登录失败,请检查密码") } finally { studentLoading.value = false } @@ -204,7 +209,7 @@ async function submitStudent() { async function submitAdmin() { if (!adminName.value || !adminPassword.value) return adminLoading.value = true - showAdminError.value = false + adminError.value = "" try { const data = await Account.login(adminName.value, adminPassword.value) user.username = data.username @@ -214,8 +219,8 @@ async function submitAdmin() { router.replace( window.location.pathname + window.location.search + window.location.hash, ) - } catch { - showAdminError.value = true + } catch (err: any) { + adminError.value = loginError(err, "登录失败,请检查用户名和密码") } finally { adminLoading.value = false } diff --git a/client/src/components/editor/Editors.vue b/client/src/components/editor/Editors.vue index 25dfeb5..9705412 100644 --- a/client/src/components/editor/Editors.vue +++ b/client/src/components/editor/Editors.vue @@ -79,11 +79,6 @@