refactor: 精简发给模型的上下文、隐藏接口原始报错,合并重复的删除逻辑

- 发给模型的对话只留最近 20 条,除最后一条 AI 回复外旧回复的代码块换成占位文字,
  对话越长越省 token,也不会超出上下文长度
- 学生看到的 AI 报错只显示超时 / 繁忙 / 出错(状态码),完整错误写日志;
  客户端自己断开导致的中止不再记错误
- 管理员查询对话必须带 task_id 或 user_id,不再一次查出全站所有对话
- “已评分或获奖不能删”和“删除一轮对话”合并到 services/cascade.ts,补上删除一轮对话的测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 22:26:37 +08:00
1 parent 325474bdb7
commit ffdfbc2513
8 files changed
+122 -35

No files matched your search

+19
View File
@@ -226,6 +226,25 @@ describe("提交与删除", () => {
expect(all(`SELECT * FROM prompt_message`)).toEqual([])
})
test("删除一轮对话:连带提问和关联提交;被评过分的不能删", async () => {
makeUser("stu")
makeUser("other")
const task = makeTask("T")
const stu = await client("stu")
const first = await stu("POST", "/api/submission/", { task_id: task, html: "<p>1", prompt: "第一版" })
const second = await stu("POST", "/api/submission/", { task_id: task, html: "<p>2", prompt: "第二版" })
const asstOf = (sub: string) =>
get(`SELECT id FROM prompt_message WHERE submission_id = $id`, { id: sub.replaceAll("-", "") })!.id
await (await client("other"))("PUT", `/api/submission/${second.json.id}/score`, { score: 4 })
expect((await stu("DELETE", `/api/prompt/messages/${asstOf(second.json.id)}/pair`)).status).toBe(400)
const res = await stu("DELETE", `/api/prompt/messages/${asstOf(first.json.id)}/pair`)
expect(res.json).toEqual({ deleted: true, submission_deleted: true })
expect(all(`SELECT content FROM prompt_message WHERE role = 'user'`)).toEqual([{ content: "第二版" }])
expect(all(`SELECT COUNT(*) AS n FROM submission_submission`)).toEqual([{ n: 1 }])
})
test("被评分的提交只有超管能删", async () => {
const author = makeUser("author")
makeUser("other")
+36 -1
View File
@@ -1,6 +1,13 @@
import { describe, expect, test } from "bun:test"
import { pyRound, pySum } from "../src/services/pyround"
import { chatModel, extractCode, parseGuidanceResponse, streamChat } from "../src/services/llm"
import {
chatModel,
extractCode,
parseGuidanceResponse,
streamChat,
trimHistory,
userFacingError,
} from "../src/services/llm"
import { config } from "../src/config"
import { checkPassword, makePassword } from "../src/auth"
import { formatMicros, iso, uuidHex, uuidOut } from "../src/db"
@@ -114,3 +121,31 @@ describe("streamChat", () => {
expect(chatModel("")).toBe(config.llmModel)
})
})
test("trimHistory 只留最近 20 条,旧回复的代码换成占位", () => {
const history = Array.from({ length: 30 }, (_, i) => ({
role: (i % 2 ? "assistant" : "user") as "user" | "assistant",
content: i % 2 ? `第${i}版\n\`\`\`html\n<p>${i}</p>\n\`\`\`` : `改一下 ${i}`,
}))
const out = trimHistory(history)
expect(out).toHaveLength(20)
expect(out[0]!.content).toBe("改一下 10")
expect(out.at(-1)!.content).toContain("<p>29</p>")
expect(out.at(-3)!.content).toBe("第27版\n```\n(旧版本代码已省略)\n```")
expect(out.filter((m) => m.content.includes("<p>"))).toHaveLength(1)
})
test("userFacingError 不透出接口原始返回", () => {
const quiet = console.error
console.error = () => {}
try {
expect(userFacingError(new Error('Error code: 401 - {"error":"invalid api key sk-xxx"}'))).toBe(
"AI 服务出错(401),请稍后重试",
)
expect(userFacingError(new Error("Error code: 429 - busy"))).toBe("AI 服务繁忙,请稍后重试")
expect(userFacingError(new Error("模型响应超时"))).toBe("AI 响应超时,请重试")
expect(userFacingError(new Error("fetch failed: ECONNREFUSED 10.0.0.1"))).toBe("AI 服务出错,请稍后重试")
} finally {
console.error = quiet
}
})