From 43c18cdb88fe46aaace5077832fdcc6b1f01e26d Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 27 Oct 2026 21:36:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E5=9B=9E=E5=A4=8D=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E6=9B=BF=E5=AD=A6=E7=94=9F=E5=AE=9A=E4=BA=86=E4=BB=80?= =?UTF-8?q?=E4=B9=88=E5=B9=B6=E9=97=AE=E4=B8=80=E4=B8=AA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E2=80=9C=E5=B8=AE=E6=88=91=E5=86=99=E6=B8=85=E6=A5=9A?= =?UTF-8?q?=E2=80=9D=E8=83=BD=E7=9C=8B=E5=88=B0=E9=A2=98=E7=9B=AE=E5=92=8C?= =?UTF-8?q?=E4=B9=8B=E5=89=8D=E7=9A=84=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 生成回复分“做了什么 / 我替你定的 / 想一想”三项;只问问题时直接回答、不给代码 - 没有代码的回复发 code: null,不再清空网页;上下文保留最近一份带代码的回复 - 教练带上题目要求、最近的提问和 AI 上一次的说明,照搬题目不算写清楚 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- server/src/services/classifier.ts | 11 +----- server/src/services/llm.ts | 64 +++++++++++++++++++------------ server/src/services/prompt.ts | 43 +++++++++++++++++++++ server/src/ws.ts | 11 +++++- server/tests/unit.test.ts | 15 +++++++- server/tests/ws.test.ts | 50 ++++++++++++++++++++++-- 7 files changed, 155 insertions(+), 41 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 749cc44..9d379ee 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`), `review` (mounted under `/submission/review`: teacher class × task wall — no `classname` means all classes —, students' peer works with `scope=class|all`), `prompt` - Who can read a submission (`canView` in `routes/submission.ts`): staff, the owner, anyone if it's in the showcase, or any student who has submitted the same task (any class). Students' `GET /submission/` only lists their own. Peer works and the random-rating item are anonymous until the viewer rates them (`PUT /:id/score` returns the author). `flag` now only carries `present` (teacher's in-class presentation queue); the old four colors are no longer shown - - `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; chat context is the last 20 messages with older code blocks elided), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background; the model sees the task brief and the previous prompt, temperature 0, prompts under 4 chars get no level), `cascade.ts` (Django's CASCADE deletes done by hand, plus the shared delete rules: `hasRatingsOrAwards`, `deleteRound`), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit) + - `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; chat context is the last 20 messages with code kept only in the newest reply that has code. Chat replies are written as **做了什么 / 我替你定的 / 想一想** — the decisions the AI made for the student and one open question, to prompt the next round; a reply to a plain question has no code and the WebSocket sends `code: null`. The “帮我写清楚” coach gets the task brief, recent prompts and the last AI explanation from `guidanceContext` in `services/prompt.ts`, and judges by what the student added beyond the brief, like the classifier), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background; the model sees the task brief and the previous prompt, temperature 0, prompts under 4 chars get no level), `cascade.ts` (Django's CASCADE deletes done by hand, plus the shared delete rules: `hasRatingsOrAwards`, `deleteRound`), `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; the AI context and the page's chat include `conversation`, `edit` and `manual` messages) 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/server/src/services/classifier.ts b/server/src/services/classifier.ts index 14afb58..f6f3bfd 100644 --- a/server/src/services/classifier.ts +++ b/server/src/services/classifier.ts @@ -1,5 +1,6 @@ import { all, get, run } from "../db" import { complete, llmConfigured } from "./llm" +import { briefOf } from "./prompt" // 学生在这里是“指挥 AI 做网页”,看的是这句提问里学生自己想了多少,不是要 AI 做的东西有多大: // 把题目要求原样贴过去,功能列得再多也只是照着做(L3) @@ -24,7 +25,6 @@ const CLASSIFY_SYSTEM_PROMPT = `你是一个教育评估专家。学生在网页 只返回一个数字(1-6),不要解释。` const MIN_PROMPT_CHARS = 4 -const BRIEF_CHARS = 800 const PROMPT_CHARS = 1500 const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) @@ -52,15 +52,6 @@ async function callLlm(content: string): Promise { } } -// 题目要求去掉图片、压成一段,太长的截掉 -function briefOf(content: string) { - const text = content - .replace(/!\[[^\]]*\]\([^)]*\)/g, "") - .replace(/\s+/g, " ") - .trim() - return text.length > BRIEF_CHARS ? text.slice(0, BRIEF_CHARS) + "……" : text -} - function clip(text: string) { return text.length > PROMPT_CHARS ? text.slice(0, PROMPT_CHARS) + "……" : text } diff --git a/server/src/services/llm.ts b/server/src/services/llm.ts index 2179102..5c02605 100644 --- a/server/src/services/llm.ts +++ b/server/src/services/llm.ts @@ -1,18 +1,29 @@ import { config } from "../config" -export const SYSTEM_PROMPT = `你是一个网页生成助手。根据用户的需求描述,生成网页代码。 +// AI 回复里的说明是学生唯一会读的文字,用它引导学生想下一步: +// “我替你定的”指出学生没说、AI 自己拿的主意,学生看了才会说“不要这样,因为……”;“想一想”只问不答 +export const SYSTEM_PROMPT = `你是一个网页生成助手,和学生一起做网页。根据学生的描述生成网页代码。 规则: -1. 使用一个 \`\`\`html 代码块返回所有代码 +1. 生成或修改网页时,使用一个 \`\`\`html 代码块返回所有代码 2. HTML 代码只需要 body 内的内容,不需要完整的 HTML 文档结构 3. CSS 样式写在