[Auto CQUPT Plan] HIT SEE 做题自动化(补档)

1961 字
10 分钟
[Auto CQUPT Plan] HIT SEE 做题自动化(补档)

1. 背景#

本人苦傻福 C语言 在线编程作业久矣,一个学期TM要刷300道题目,绝大部分题目毫无营养可言并且浪费时间,简直是厨圣!所以为了解决这个困境,顺便学习一下爬虫,就拿 YakitHIT SEE 这个平台做了一下抓包和逆向,实现了 LLM自动做题键盘敲击记录伪造计划任务批量做题

Warning

免责声明:

1. 本程序仅供学习使用,若你用这个玩意然后挂科了,那只能说明你是’小可爱‘,与作者一律无关!

2. 若你已经掌握C语言并且觉得C语言的OJ作业限制了你对新技术、新事务的学习,那你可以食用本项目。

2. 接口概览#

#作用Method路径关键入参返回关键字段
1登录POST/student/authcode,pwdsid
2题目列表GET/student/list-tagqBaseType,level,sid[{id,content}]
3建会话POST/student/choose-q[{qBaseType,number,level,tagId}]sessionId
4取题详情GET/student/record/{sessionId}sidcontent,globalId,qType
5提交答案POST/student/post-answeranswerPaper,verificationCodecode,timeLeft
6确认交卷GET/student/submitsessionId,sid字面量 1
7获取分数GET/student/score-result/{sessionId}sid[{score,scoreResult}]

所有路径前缀为 https://cprg.cqupt.edu.cn/train/api

3. 接口文档#

3.1 用户鉴权(登录)#

获取 sid,后续所有操作的前提。

请求#

POST https://cprg.cqupt.edu.cn/train/api/student/auth?sid=

请求 Body(JSON):

{
"code": "",
"pwd": ""
}
字段类型说明
codestr学号
pwdstr密码

响应#

{
"code": "",
"expire": 0,
"id": 0,
"ip": "",
"name": "",
"sid": ""
}
字段类型说明
codestring学号
expireint64过期时间(Unix 时间戳)
idint64用户 ID
ipstring登录 IP
namestring姓名
sidstring身份识别码,其余所有接口的鉴权凭据(需自行在会话内保存)

3.2 获取题目列表#

查询可用的 题目标签(tag)列表,拿到 tagId 供出题使用。

请求#

GET https://cprg.cqupt.edu.cn/train/api/student/list-tag?qBaseType=&level=&sid=

Query 参数:

参数类型说明
qBaseTypeint64问题类型:0 单选 1 多选 2 判断 3 填空 4 编程 5
levelint64难度:0 随机 1 初学者 2 容易 3 中等 4 困难 5 极难
sidstring身份识别码

响应(数组)#

[
{
"id": 87,
"content": "U1-字符处理",
"contentEn": "U1-Character Processing"
},
{
"id": 3,
"content": "U1-数据类型、运算符与表达式",
"contentEn": "U1-Data types, operators and expressions"
}
]
字段类型说明
idint标签 ID(tagId)
contentstring中文题目
contentEnstring英文题目

3.3 创建答题会话#

标签 + 题目数量 向服务端要一个答题会话,得到 sessionIdsessionId 是整个做题流程的核心句柄。

请求#

POST https://cprg.cqupt.edu.cn/train/api/student/choose-q?sid=

Body 传的是数组(源码固定只放 1 个元素):

[
{
"qBaseType": 4,
"number": 1,
"level": 0,
"tagId": 87
}
]
字段类型说明
qBaseTypeint64问题类型
numberint64问题数量(1 道)
levelint64问题难度
tagIdint64标签 ID(来自 2)

响应#

{
"code": 0,
"sessionId": "3e03d593195b4e81bedb0dd4b54d7177"
}
字段类型说明
codeint64状态码(0 表示成功)
sessionIdstring答题会话 ID(核心句柄)

3.4 获取题目详情#

拿到题目内容 content 与题目的全局 ID globalId、题型 qType

请求#

GET https://cprg.cqupt.edu.cn/train/api/student/record/{sessionId}?sid=
位置字段说明
PATHsessionId会话 ID
Querysid身份识别码

响应(关键字段)#

{
"code": 0,
"startTime": "2025-11-08T00:49:31.768254367",
"state": 0,
"timeLeft": 4525,
"totalTime": 4525,
"paper": {
"questions": [
{
"id": 1350,
"content": "大小写字母转换问题:\n ...",
"point": 10.0,
"qType": 5,
"language": 1,
"globalId": "3e74118614fd49adbb575ca4cec8d1b3",
"options": []
}
]
},
"answerPaper": {
"q0": [], "q1": [], "q2": [], "q3": [],
"q4": [
{
"answer": "#include <stdio.h>\n\nint main()\n{\n\treturn 0;\n}",
"keySequence": [],
"globalId": "3e74118614fd49adbb575ca4cec8d1b3",
"qType": 5
}
],
"q5": []
},
"examName": "",
"studentCode": "",
"studentName": "",
"buyCode": false,
"score": 0.0,
"timeline": [ { "createTime": "...", "actionCode": 0, "actionData": "" } ]
}

实际做题只用其中三个字段:

字段说明
content题目题干(丢给 LLM 生成答案)
globalId题目的全局 ID(提交时原样回传,用于定位题目)
qType题型(提交时原样回传,q4 / 编程题里为 5

结构说明paper.questions[0] 是题目内容;answerPaper.q4[0]编程题对应的作答槽位。单选(q0)/ 多选(q1)/ 判断(q2)/ 填空(q3)/ 编程(q4)/ q5 一一对应题型的作答槽位。

3.5 提交答案#

这里可以用于伪造键盘敲击记录

把答案写回服务器。注意:这只是”暂存/写答案”,不等于最终交卷;且必须附带动态生成的 verificationCode

请求#

POST https://cprg.cqupt.edu.cn/train/api/student/post-answer?sid=

Body(JSON):

{
"sessionId": "f3caabbb4070471f9123da021b8f50d2",
"answerPaper": {
"q0": [],
"q1": [],
"q2": [],
"q3": [],
"q4": [
{
"answer": "#include <stdio.h>\n\nint main()\n{\n\tprintf(\"%c\", c + 32);\n\treturn 0;\n}",
"keySequence": [],
"globalId": "f227f40921514aa994931b6c82e47361",
"qType": 5
}
],
"q5": []
},
"verificationCode": "551e50eb37c5d80ea40230d3bbe2cc71"
}
字段类型说明
sessionIdstring会话 ID
answerPaper.q4[].answerstring答案代码
answerPaper.q4[].keySequence[]int64键盘敲击记录(伪造时不传则为空数组)
answerPaper.q4[].globalIdstring来自 4 的 globalId
answerPaper.q4[].qTypeint64来自 4 的 qType
verificationCodestring动态验证码,见 §3.8,校验失败可能被拒

响应#

{ "code": 0, "timeLeft": 3816 }
字段类型说明
codeint64状态码(0 表示成功)
timeLeftint64剩余时间(秒)

3.6 确认交卷#

写入答案后调用,正式提交该会话。

GET https://cprg.cqupt.edu.cn/train/api/student/submit?sessionId=&sid=
位置字段说明
QuerysessionId会话 ID
Querysid身份识别码

成功响应:字面量 1(字符串)。

3.7 获取分数#

请求#

GET https://cprg.cqupt.edu.cn/train/api/student/score-result/{sessionId}?sid=
位置字段说明
PATHsessionId会话 ID
Querysid身份识别码

响应(数组)#

[
{
"globalId": "f227f40921514aa994931b6c82e47361",
"score": 0.0,
"qType": 5,
"scoreResult": {
"state": -1,
"score": 0.0,
"similarity": 0.0,
"keyPointScore": 0.0,
"staticMatchScore": 0.0,
"dynamicTestScore": 0.0,
"failedTestCase": {
"output": "Press a key and then press Enter:\nz",
"testCase": { "input": "Z\r\n", "output": "Press a key and then press Enter:z" }
},
"compilerOutput": "",
"compilerErrorOutput": "",
"directOutputCheat": false
}
}
]
字段(scoreResult 内)说明
state评测状态(-1 通常表示未通过 / 失败)
score得分
similarity / keyPointScore / staticMatchScore / dynamicTestScore各维度评分(相似度、要点、静态匹配、动态测试)
failedTestCase失败用例,含输入 / 期望输出 / 实际输出
compilerOutput / compilerErrorOutput编译输出 / 编译错误输出
directOutputCheat是否判定为「直接输出作弊」

成功返回 JSON 数组;失败(无记录)返回空。

3.8 验证码(verificationCode)生成算法#

verificationCode 不是随机值,而是由 当前日期时间(精确到分钟)+ sessionId 拼接后做 MD5 得到,服务端据此校验提交是否”新鲜、未过期”。

formattedTime = "YYYY-MM-DD HH:MM" + sessionId
verificationCode = MD5(formattedTime)

Go 实现(ojSupport.go):

func (root *OjSupport) GenerateVerificationCode(sessionID string) string {
now := time.Now()
formattedTime := fmt.Sprintf("%d-%02d-%02d %02d:%02d %s",
now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(),
sessionID,
)
hash := md5.Sum([]byte(formattedTime))
return fmt.Sprintf("%x", hash)
}

即拼接串形如:

2025-11-08 00:49 f3caabbb4070471f9123da021b8f50d2

注意:年份是四位%d),月份 / 日期 / 时 / 分是补零两位%02d),sessionId 以空格分隔直接拼在末尾。

3.9 键盘敲击记录(keySequence)伪造#

平台的反作弊会记录答题时的键盘事件,存入 keySequence。这段时间被当作”真正在敲代码”的行为特征。

  • keySequence成对的元素组成:[时间戳, 键码, 时间戳, 键码, ...]
  • 时间戳为毫秒级的 time.Now().UnixNano() / 1e6
  • 键码在固定键值池里随机挑:{1,2,3,4,5,6,7,8,8,45,46,47,66,54,67,0,44,26,33,29}

Go 实现(FakeKeyPress()):

keyList := []int64{1,2,3,4,5,6,7,8,8,45,46,47,66,54,67,0,44,26,33,29}
for i := 0; i < 3; i++ {
localList := []int64{
time.Now().UnixNano() / 1e6, // 时间戳(ms)
keyList[rand.Intn(len(keyList))], // 随机键码
}
keyPressList = append(keyPressList, localList...)
time.Sleep(time.Duration(root.weightedRandom()) * time.Millisecond) // 控制敲击间隔
}

为了让敲击间隔更像真人,用加权随机分布:70% 概率间隔 0–600ms,30% 概率间隔 600–2000ms:

func (root *OjSupport) weightedRandom() int {
rand.Seed(time.Now().UnixNano())
if rand.Float64() < 0.7 {
return rand.Intn(601) // 0-600ms
}
return 600 + rand.Intn(1401) // 600-2000ms
}

GenTimeSpan() 会在”做题耗时”窗口内,每隔 0–2s 调用一次 FakeKeyPress(),模拟持续敲击。

4. 完整做题流程时序#

sequenceDiagram participant C as 客户端 participant O as OJ 平台 C->>O: 1. POST /train/api/student/auth {code,pwd} O-->>C: {"sid": "...", ...} Note over C,O: 可选:列出题签 C->>O: 2. GET /train/api/student/list-tag?qBaseType=4&level=0&sid= O-->>C: [{"id":87,"content":"U1-字符处理"}, ...] C->>O: 3. POST /train/api/student/choose-q?sid= [{qBaseType,number,level,tagId}] O-->>C: {"sessionId": "..."} C->>O: 4. GET /train/api/student/record/{sessionId}?sid= O-->>C: {paper.questions[{content,globalId,qType}], answerPaper} Note over C: 把 content 交给 LLM → 得到 solution loop 做题耗时窗口内 C->>O: 5a. POST /train/api/student/post-answer?sid= (带 keySequence 的伪造敲击) end C->>O: 5b. POST /train/api/student/post-answer?sid= 真实答案 + verificationCode O-->>C: {"code":0,"timeLeft":...} C->>O: 6. GET /train/api/student/submit?sessionId=&sid= O-->>C: 1 C->>O: 7. GET /train/api/student/score-result/{sessionId}?sid= O-->>C: [{"score":10.0,"scoreResult":{...}}]
  1. 登录sid(§3.1)
  2. 选标签(可选)拿 tagId(§3.2)
  3. 建会话sessionId(§3.3)
  4. 取题content / globalId / qType(§3.4)
  5. (可选)做题窗口内伪造 keySequence(§3.9)
  6. 回填答案 + verificationCode(§3.5、§3.8)
  7. 确认交卷(§3.6)
  8. 查分(§3.7)

5. 相关项目#

Auto-CQUPT-Plan
/
OJByeBye
Waiting for api.github.com...
00K
0K
0K
Waiting...

本文档基于对 OJByeBye 项目源码(core/utils/ojSupport/ojSupport.gocore/models/models.gocore/runc/runc.go)及现有分析文档(README.mddocs/C语言做题系统逆向分析.md)的整理归纳而成。

目标平台基础地址:

https://cprg.cqupt.edu.cn/train

所有接口均位于 https://cprg.cqupt.edu.cn/train/api/student/* 下,核心鉴权凭据为登录后返回的 sid(身份识别码),几乎所有请求都要携带它(GET 走 Query,POST 走 Query 或 PATH)。

6. 结尾#

没有了喵,谢谢阅读

gopher
gopher

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

[Auto CQUPT Plan] HIT SEE 做题自动化(补档)
https://blog.tuf3i.cc/posts/acp-ojbyebye/
作者
TuF3i
发布于
2026-08-18
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
TuF3i
一只区,什么也不想写
分类
标签
站点统计
文章
16
分类
9
标签
18
总字数
43,299
运行时长
0
最后活动
0 天前

目录