feat: 挑战的题目要求发给 AI 当对照清单,学生没提到的不替他做,在“想一想”里问
Deploy / deploy (build, debian, 22) (push) Canceled after 0s

学生不用再整段贴题目;AI 只做学生说了的,从没提到的要求里挑一项问他打算怎么做。
整段贴了题目的照做。自由创作的题目是学生自己写的,不发。输入框提示改成“用你自己的话说说”。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 21:40:04 +08:00
1 parent 43c18cdb88
commit 5bdacc438d
6 files changed
+55 -9

No files matched your search

+13 -3
View File
@@ -39,6 +39,15 @@ document.getElementById('btn').onclick = function() {
</script>
\`\`\``
// 挑战题目要求只当对照清单:学生没说的不替他做,挑一项放进“想一想”问他打算怎么做,
// 要做成什么样还是学生自己说;整段贴了题目的照做(不拦,拦了只会去用外部 AI)
export const BRIEF_RULES = `下面是这道题的题目要求。它是对照清单,不是让你照着做——这是课堂练习,题目要求要学生自己一步步提出来,这正是这节课要练的:
- 只做学生在对话里说了的内容,做出来要能打开、能用。学生说的话里包含了题目要求(比如整段贴了题目),就照着做
- 题目要求里学生还没提到的功能、规则和细节(如计时方式、对错的颜色、计分、重来按钮),哪怕很常见、顺手就能做,也先不做;也不要照搬题目里的类名、变量名和数据
- 从学生没提到的要求里挑一项最重要的放进“想一想”,问他打算怎么做(如“题目还要求标出打错的字,你希望打错的时候是什么样子?”)
- 学生要的和题目要求冲突时,按学生说的做,在“想一想”里提醒他题目是怎么要求的
- 题目要求都做到了,“想一想”就问他自己的想法,不再提题目`
// 判断标准和分类器一致:看学生自己想了什么,照搬题目写得再长也不算清楚
export const GUIDANCE_SYSTEM_PROMPT = `你是一个提示词写作教练。学生在网页制作课上让 AI 生成或修改网页,你帮他把这一次要对 AI 说的话想清楚、说具体。后面会给你题目要求、他之前对 AI 说过的话和 AI 上一次的说明。
@@ -208,9 +217,10 @@ export function userFacingError(e: unknown) {
return status ? `AI 服务出错(${status}),请稍后重试` : "AI 服务出错,请稍后重试"
}
// 生成整页代码:开着思考,质量优先
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal) {
return streamCompletion([{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)], {
// 生成整页代码:开着思考,质量优先。brief 是挑战的题目要求(见 challengeBrief),自由创作没有
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal, brief?: string | null) {
const system = brief ? `${SYSTEM_PROMPT}\n\n${BRIEF_RULES}\n\n${brief}` : SYSTEM_PROMPT
return streamCompletion([{ role: "system", content: system }, ...trimHistory(history)], {
model: chatModel(model),
thinking: true,
signal,
+16 -3
View File
@@ -102,15 +102,28 @@ export function codeAsMarkdown(code: {
// 题目要求去掉图片、压成一段,太长的截掉
const BRIEF_CHARS = 800
export function briefOf(content: string) {
export function briefOf(content: string, max = BRIEF_CHARS) {
const text = content
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
.replace(/\s+/g, " ")
.trim()
return text.length > BRIEF_CHARS ? text.slice(0, BRIEF_CHARS) + "……" : text
return clipTo(text, max)
}
const clipTo = (text: string, n: number) => (text.length > n ? text.slice(0, n) + "……" : text)
function clipTo(text: string, n: number) {
return text.length > n ? text.slice(0, n) + "……" : text
}
// 挑战的题目要求,给生成网页的 AI 当对照清单(见 llm.ts 的 BRIEF_RULES);要逐条对照,不能截得太短。
// 自由创作的题目是学生自己写的介绍,不用对照,返回 null
export function challengeBrief(taskId: number) {
const task = get<{ title: string; content: string }>(
`SELECT title, content FROM task_task WHERE id = $id AND task_type = 'challenge'`,
{ id: taskId },
)
const brief = task && briefOf(task.content, 4000)
return brief ? `题目:${task.title}\n\n题目要求:${brief}` : null
}
// “帮我写清楚”的教练要知道题目和学生已经做到哪了,才分得清照搬题目和自己的想法:
// 题目要求、学生最近几次对 AI 说的话、AI 上一次的说明(代码去掉)
+3 -1
View File
@@ -3,6 +3,7 @@ import { all, get, run, iso, uuidOut } from "./db"
import { userFromAccessToken, type User } from "./auth"
import {
canAccessTask,
challengeBrief,
codeAsMarkdown,
getOrCreateActiveConversation,
guidanceContext,
@@ -132,7 +133,8 @@ async function handlePrompt(ws: WS, data: any) {
}))
let full: string
try {
full = await streamToClient(ws, streamChat(history, model, ws.data.abort.signal))
const brief = challengeBrief(ws.data.taskId)
full = await streamToClient(ws, streamChat(history, model, ws.data.abort.signal, brief))
} catch (e) {
// 客户端自己断开导致的中止不算出错
if (!ws.data.closed) send(ws, { type: "error", content: userFacingError(e) })
+21
View File
@@ -192,6 +192,27 @@ describe("WebSocket 代码生成", () => {
c.ws.close()
})
test("挑战的题目要求作为对照清单发给 AI,自由创作不发", async () => {
const { task, token } = await setup()
run(`UPDATE task_task SET content = $c WHERE id = $id`, { c: "做一个计时器,有开始和暂停按钮", id: task })
const c = (await connect(`/ws/prompt/${task}/?token=${token}`))!
await c.next("init")
c.ws.send(JSON.stringify({ type: "message", content: "做个页面" }))
await c.next("complete")
expect(lastRequest.messages[0].content).toContain("它是对照清单")
expect(lastRequest.messages[0].content).toContain("题目要求:做一个计时器,有开始和暂停按钮")
c.ws.close()
const creation = makeTask("我的作品", "creation", 2)
run(`UPDATE task_task SET content = '我想做一个相册' WHERE id = $id`, { id: creation })
const d = (await connect(`/ws/prompt/${creation}/?token=${token}`))!
await d.next("init")
d.ws.send(JSON.stringify({ type: "message", content: "做个页面" }))
await d.next("complete")
expect(lastRequest.messages[0].content).not.toContain("题目要求")
d.ws.close()
})
test("token 无效或任务不存在时握手失败", async () => {
const { task, token } = await setup()
expect(await connect(`/ws/prompt/${task}/?token=bad`)).toBeNull()