跳转至

外部作业源

最后更新:2026-09-22 22:57

清华大学的课程作业分布在多个系统中:网络学堂仅覆盖其中一部分,雨课堂承载课堂练习与 试卷,TUOJ 承载编程作业(AI 版与经典版两个实例),Tyche 承载部分院系的作业,DSA OJ 承载数据结构课的 OJ 作业。

外部作业源功能将上述系统的作业聚合到应用的作业页面,提供统一的截止时间展示、提交 状态判定与批改结果。插件通过 onethu.exthw.snapshot() 与 onethu.exthw.refresh() 使用该功能(见 api-reference.md §4)。

本文档说明各源的接入与凭据维护方式、故障恢复机制,以及新增作业源的实现步骤。

1. 数据模型

所有源统一映射为 ExternalHomework:

字段 类型 说明
id string 源内唯一标识,用于列表去重
source string 源标识,见 §2
courseName / title string 课程名与作业标题
deadline string 截止时间,格式 "YYYY-MM-DD HH:MM"
kind "homework" | "exam" 作业或试卷
url string? 详情链接(学生端页面)
submitted boolean 提交状态
submittedCount / totalCount number? 已提交题数 / 总题数(仅雨课堂有精确数据)
graded boolean? 批改状态,仅雨课堂可判定
audited boolean? 是否旁听课堂(雨课堂 role===6)
score / totalScore number? 得分与卷面满分,仅在已提交且已出分/已批改时设置
leafTypeId / classroomId string? 雨课堂整卷明细参数,供原生详情页拉取

2. 源与登录方式

源 标识 登录方式 恢复机制
雨课堂 yuketang 扫码登录(长轮询);官方网页登录(应用内 WebView 读取 Cookie)。短信通道已停用,官方已增加图形验证码校验 传输层中断按未扫码处理,沿用同一令牌继续轮询;移动端由前台服务保活
TUOJ(AI 版) tuoj 复用清华统一认证会话漫游;可选配置 TUOJ 独立账密 接口返回 401 / 403 时自动重新漫游一次
TUOJ(经典版) tuojClassic 同上 同上
Tyche tyche 复用清华统一认证会话漫游 同 TUOJ
DSA OJ dsa 邮箱 + 密码登录(站点无统一认证,仅手动账密);亦可手动填写会话 Cookie 会话失效抛 DsaSessionError,由设置页引导重新登录;不自动重漫游

3. 状态判定

提交状态:由各源独立查询得出。查询失败或无法判定时(例如雨课堂试卷类条目缺少 访问权限)统一判定为未提交,避免出现「已提交」的误报。DSA OJ 属此类:其 assignment.status 字段语义尚未确认(R15 20.1 实测未定),故保守判定为未提交, 待真机联调后补充判定。

批改状态:

  • 雨课堂:读取 get_exercise_list 返回的 status 字段(取值 4 与 3 表示已批改), 并结合分值占位符判定;作业与试卷的详情链接指向学生端页面 /ai-workspace/lms-graph/...。
  • Tyche:读取 task/Status 的 submissionList[](含 pid、score、result、 submitedTime),按 pid 取最新一次提交汇总。所有题目均有有效得分时判定为已 批改,score 为各题得分之和。
  • DSA OJ:不提供批改结果——课程详情接口的 assignmentList[] 只有标题与截止时间, 无得分字段,故 graded 与 score 恒为空。

3.1 雨课堂原生详情与提交入口(R20–R21)

雨课堂的作业正文是加密字体 + LaTeX + 外链图片的组合,官方页面在应用内直接打开时字体与公式缺失、 图片 401。R20 起改为原生详情页:正文在应用内渲染,不跳转至浏览器。

能力 实现要点
题干渲染 正文进入本地 srcdoc 沙箱 iframe(无网络权限、脚本白名单);加密字体取自响应中的 data.font(exam_font_<hash>.ttf),下载后以 @font-face 应用,缓存 7 天、失败 10 分钟内不重试
公式 KaTeX 随包内置(vendor/katex,懒加载),$…$ 与 $$…$$ 本地渲染,正文不外传
图片 经应用侧带会话 Cookie 代理取回后内联(data:),避免 iframe 内出现 401
降级 字体/公式/图片任一环节失败都逐级降级(纯文本 → 提示条),任何情况下均不出现白屏
分数与评语 已批改作业在入口处直接显示 X/Y(与考试同一显示位);详情页按题给出得分、我的作答与老师评语(总评与具名批注同文时去重,避免同一条评语渲染两次)
提交入口(R20-C1,第一阶段) 内嵌官方作答页(WebView)+ 注入会话 Cookie,资格判定为纯函数
主观题原生作答(R20-C2,第二阶段) 主观题在应用内撰写与提交(工具栏对齐官方、插图四通道含拍照、KaTeX 公式、草稿与剩余次数),经用户确认对话框后调用官方提交接口;方案与实测见 外部作业源-需求与实现方案.md §31,功能说明见 homework.md §4
会话保活(R21-B) 会话健康检查 + 保活 + Cookie 导出/导入,降低“打开时会话已失效”的概率;Tyche 侧登录失效静默自动重登(R21-A)

提交边界:外部作业源的展示层为只读——只读取标题、课程、截止时间、提交与批改状态 以及题干正文,用于在应用内呈现;试卷不显示任何提交入口。作业的作答与提交能力仅限用户 亲笔:应用只承担内容排版渲染、图片上传与提交动作,不做 AI 代写、代交、自动提交或批量 提交;边界清单与工程护栏见 homework.md §5。

4. 会话失效恢复

TUOJ 系源在接口返回 401 或 403 时自动重新漫游一次并重新拉取数据。此前的自动漫游仅 在源未配置时触发,已配置但凭据失效的情况缺少恢复手段。

约束 取值
重试间隔 同一源两次自动重试间隔不小于 10 分钟
重试上限 每进程每源不超过 3 次
并发处理 同一源的并发 401 共享同一请求
退出抑制 用户显式退出登录后不自动重登
失败提示 自动重登仍失败时,作业页显示提示条并附设置页重登入口

过夜老化与启动续期(R23):TUOJ 会话过夜老化后,接口可能返回 HTTP 200 但内容是登录页 HTML(非 JSON),该响应同样判定为会话失效。此外每次启动 OneTHU 会对已配置的 TUOJ CAS 源 主动续期一次(标记 tuojStartupRenewed):走强制刷新并放宽 24 小时频控,仍尊重用户的显式 退出抑制。护栏 tools/tuoj-r23-test.mjs。

DSA OJ 不接入清华统一认证,没有可用的自动恢复通道:会话失效时(user.php checklogin 返回未登录、业务 error≠0、或响应非 JSON)抛出 DsaSessionError, 由作业页错误提示与设置页重登入口引导用户重新输入邮箱与密码。

5. 新增作业源

  1. 实现数据获取:在 packages/core/src/exthw/ 下新增源实现文件,返回 ExternalHomework[]。提交与批改状态需真实查询得出,无法判定时判定为未提交。 类型定义加入 types.ts 并从 index.ts 导出。
  2. 注册源标识:扩展 ExtHwSourceId 联合类型,在 extHwSourceName() 中补充显示 名称,并在设置页按平台归组。
  3. 处理凭据:需要独立凭据的源使用 ExtHwCreds 结构并在设置页提供表单;复用清华 统一认证的源不需要独立凭据。无统一认证的站点(如 DSA OJ)在 login.ts 中实现 账密登录工具函数,登录成功后拼接会话 Cookie 并交回设置页保存。
  4. 补充测试:在 tools/exthw-status-test.mjs 中增加断言(使用模拟数据,不依赖 真实网络);具备登录态的源可在 tools/exthw-smoke.mjs 中增加真实数据验证。
  5. 更新文档:在本文档 §2 表格中新增一行,源特有的登录与恢复行为补充至 §3、§4。

6. 界面与测试

设置页:设置 → 外部作业源,按平台归组。移动端扫码面板为全屏显示,并提示使用 另一台设备扫码、保持应用在前台。DSA OJ 位于「OJ 平台」分组(默认折叠),提供邮箱 + 密码登录,并保留手动填写会话 Cookie 的高级入口。

测试工具:

工具 覆盖范围
tools/exthw-status-test.mjs 聚合状态机、状态判定、频控逻辑;DSA OJ 的 endDate 容错解析与会话失效判定
tools/tuoj-cas-test.mjs CAS 漫游、二次认证提示、重新漫游
tools/tuoj-r23-test.mjs TUOJ 过夜老化判定(200 + 非 JSON)与启动续期
tools/ykt-qr-test.mjs 扫码状态机、保活服务生命周期、传输层超时
tools/ykt-body-test.mjs 题干正文渲染(加密字体栈合法性、LaTeX 与图片处理、降级链)
tools/ykt-detail-ui-test.mjs 详情页纯函数:分数文案、评语去重、作答态映射
tools/ykt-submit-test.mjs 主观题提交:签名、multipart 字段顺序、callback 校验
tools/c2-redline-test.mjs 学术红线护栏(提交能力不进插件工具清单、无绕过用户确认的路径)
tools/ykt-exercise-detail-test.mjs get_exercise_list → 详情模型映射(含组作业与缺分边界)
tools/ykt-detail-smoke.mjs / tools/ykt-session-smoke.mjs 真实凭据下的详情拉取与会话保活冒烟(环境变量同上一行)
tools/exthw-smoke.mjs 真实凭据下的端到端验证(环境变量 YKT_COOKIE、TUOJ_COOKIE、TUOJ_CLASSIC_COOKIE、TYCHE_COOKIE、DSA_COOKIE)

7. 接入记录

各源的接入批次、实测结论与已知限制。批次号与源码注释中的编号一致,便于回溯到具体 实现与实测记录。

源 引入批次 实测结论与已知限制
雨课堂 初始版本 扫码登录(长轮询)与官方网页登录(应用内 WebView 读 Cookie)两条通道;短信通道已停用;试卷提交状态经 /v/exam/cover 取得;作业与试卷详情链接指向学生端 /ai-workspace/lms-graph/...
TUOJ(AI 版) 初始版本 统一认证漫游为默认通道,另可配置独立账密直连;返回 401 / 403 触发一次自动重漫游,设备信任与二次认证均有可操作引导
Tyche 初始版本 复用统一认证漫游;批改结果取 task/Status 的 submissionList[],按 pid 汇总最新一次提交
TUOJ(经典版) R15 20.1 站点 oj.cs.tsinghua.edu.cn 与 AI 版接口行为一致,复用同一套客户端实现,仅 base URL 与漫游回调不同
雨课堂(原生详情) R20-A / B2 / B3 原生只读详情页(题干加密字体、KaTeX 公式、图片代理、srcdoc 沙箱,任何情况下均不出现白屏);已批改作业入口显示 X/Y,评语按题结构化呈现;加密字体根因是 font-family 中混入 CSS 全局关键字 inherit 导致整条声明被丢弃
雨课堂(提交入口) R20-C1 内嵌官方作答页 + 注入会话 Cookie,资格为纯函数;试卷不显示提交入口
雨课堂(主观题原生作答) R20-C2 P2 / P3 逐题原生作答编辑器(工具栏对齐官方、插图四通道含拍照上传、KaTeX 公式、草稿与剩余次数);提交经用户确认后调用官方接口,成功后重新拉取真实状态;late_submission 存在对象与数字两种形态,按双口径容错解析
Tyche(静默重登) R21-A 登录失效时按 R19 的 TUOJ 会话失效模式静默重漫游一次;图形验证码场景给可操作引导
雨课堂(会话保活) R21-B 会话健康检查 / 保活 / Cookie 导出导入;保活有效性实验协议待回填
DSA OJ R15 20.1 / 20.2 站点 dsa.cs.tsinghua.edu.cn/oj/,老式 Bootstrap/jQuery 站点,接口均为 POST form-urlencoded,会话依赖 Cookie;登录为邮箱 + 密码,无统一认证;截止时间取 assignment.endDate,其格式不确定(站点标注 UTC+8),故按多格式容错解析;提交状态保守判定为未提交(status 语义待联调确认);不提供批改结果;只读——仅拉取标题、课程与截止时间,不提交、不抓取题目正文