Repository navigation
Canvas
ChangYo edited this page Oct 25, 2026
·
2 revisions
| 项 | 值 |
|---|---|
| 平台 | oc.sjtu.edu.cn |
| 认证 | 个人 Access Token(Bearer) |
| 数据方向 | 只读为主;讨论回复可写;作业提交未验证 |
| 难度 | 低 |
| 最后验证 | 2026-09-15 |
| 证据等级 | A(Canvas 官方文档 + Dela 实测) |
Canvas 是交大教学平台,底层为 Instructure Canvas LMS。它提供标准 REST API,个人 Access Token 几分钟即可取得,适合作为第一个校园工具的数据源。
- 可获取:课程、学期、作业、提交、文件与文件夹、模块、讨论与公告、测验、页面、教学大纲。
- 前置:个人 Access Token。头像 → 设置 → 「允许融入使用的外部软件」→「+ 创建新的访问许可证」。不同 Canvas 版本的中文翻译可能不同,以实际界面为准。
- 注意:
per_page上限为 50;401 可能是单课无权限,不一定是 Token 失效;文件下载经 302 跳转到 S3,第二段不能带 Authorization。
| 对象 | 粒度 | 内容 | 历史范围 | 权限 |
|---|---|---|---|---|
| 课程 Course | 每门课 | id、名称、课程代码、学期、选课状态、成绩字段、颜色 | 全部历史课程 | 本人选课 |
| 学期 Term | 每学期 | 名称(如 2026-2027 Fall)、起止 |
全部 | 随课程返回 |
| 作业 Assignment | 每份作业 | 名称、描述 HTML、due_at、分值、提交类型、分组、quiz 关联 | 全部 | 本人课程 |
| 提交 Submission | 每份作业 | 状态、分数、提交时间、批改时间、附件、评语、迟交/缺交 | 本人 | 本人 |
| 文件 File | 每个文件 | id、uuid、文件夹、显示名、MIME、大小、更新时间、下载 URL | 全部 | 本人课程 |
| 文件夹 Folder | 每个文件夹 | id、名称、父文件夹、文件数、子文件夹数 | 全部 | 本人课程 |
| 模块 Module | 每个模块 | 名称、位置、item 列表 | 全部 | 本人课程 |
| 讨论/公告 | 每个主题 | 标题、正文 HTML、公告标记、回复数、最后回复时间 | 全部 | 本人课程 |
| 测验 Quiz | 每个测验 | 类型、due_at、题数、时限、可尝试次数 | 部分课程 | 本人课程 |
| 页面 Page | 每个页面 | 标题、正文 HTML、URL | 全部 | 本人课程 |
| 教学大纲 | 每门课 | HTML,多为空或一个 PDF 链接 | 全部 | 本人课程 |
无法获取:
- 课堂录像不在 Canvas 文件接口里,走独立的课堂视频平台。
- 其他同学的提交、成绩、讨论草稿。
- 课程文件的历史版本(只能看到当前文件)。
- 浏览器打开
https://oc.sjtu.edu.cn/,用 jAccount 登录。 - 点头像 → 设置。
- 找到「允许融入使用的外部软件」→「+ 创建新的访问许可证」。
- 用途随便填,过期时间可以留空。
- 复制生成的字符串。只显示一次,丢了只能重建。
[实测 2026-09,Dela 引导流程]
curl 'https://oc.sjtu.edu.cn/api/v1/users/self/profile' \
-H 'Authorization: Bearer <token>'返回本人 profile 就说明 Token 有效。Dela 在保存 Token 前会先调这个接口。[实测]
Authorization: Bearer <canvas_access_token>
Accept: application/json个人 Token 默认带写入 scope(2026-09-15 实测:无实体的写探测没有被 scope 层以 401 拒绝)。但「有写权限」不等于「所有写接口都能用」,具体看第 6 节。
个人自用工具用 Personal Access Token 就够了。只有你要做一个应用、替多个用户统一承载授权时,才需要联系 Canvas 管理方申请 Developer Key + OAuth。[官方]
Base URL:https://oc.sjtu.edu.cn/api/v1
| 用途 | Method | Path | 关键参数 |
|---|---|---|---|
| 验证 Token | GET | /users/self/profile |
- |
| 课程列表 | GET | /courses |
include[]=term、include[]=enrollments、per_page=50
|
| 当前在读课程 | GET | /courses |
enrollment_state=active、per_page=50
|
| 课程详情 | GET | /courses/:id |
include[]=syllabus_body |
| 作业列表 | GET | /courses/:id/assignments |
include[]=submission、per_page=50
|
| 单份提交 | GET | /courses/:id/assignments/:aid/submissions/self |
include[]=attachments、include[]=submission_comments
|
| 作业分组 | GET | /courses/:id/assignment_groups |
include[]=assignments |
| 文件夹 | GET | /courses/:id/folders |
per_page=50 |
| 课程文件 | GET | /courses/:id/files |
per_page=50 |
| 按文件夹列文件 | GET | /folders/:fid/files |
per_page=50 |
| 文件元数据 | GET | /files/:fid |
- |
| 模块 | GET | /courses/:id/modules |
per_page=50 |
| 模块 item | GET | /courses/:id/modules/:mid/items |
per_page=50 |
| 讨论/公告 | GET | /courses/:id/discussion_topics |
per_page=50 |
| 跨课程公告 | GET | /announcements |
context_codes[]=course_<course_id>;active_only=true 时还要日期 |
| 测验 | GET | /courses/:id/quizzes |
per_page=50 |
| 页面列表 | GET | /courses/:id/pages |
per_page=50 |
| 页面正文 | GET | /courses/:id/pages/:url |
- |
| 本人待办 | GET | /users/self/todo |
- |
| 即将发生 | GET | /users/self/upcoming_events |
- |
| 活动流 | GET | /users/self/activity_stream |
- |
| Planner | GET | /planner/items |
日期窗口、context_codes[]
|
| 日历事件 | GET | /calendar_events |
context_codes[]、日期窗口 |
| 回复讨论 | POST | /courses/:id/discussion_topics/:topic_id/entries |
JSON {"message":"..."}
|
{
"id": 12345,
"name": "机器学习",
"course_code": "本-(2026-2027-1)-CS3308-01-机器学习",
"term": { "name": "2026-2027 Fall" },
"workflow_state": "available",
"enrollments": [{ "type": "student", "computed_current_score": 92.5 }],
"storage_quota_mb": 2048,
"friendly_name": "机器学习",
"calendar": { "ics": "..." }
}-
course_code中段可解析出课程号(如CS3308)和班号(01),用于跨学期聚合同名课。 -
term.name是学期分类的正确依据,不要用workflow_state。 - 历史课程长期保持
available(教师未归档),所以「可用」不等于「当前学期」。 -
enrollments[].computed_current_score/final_score只在部分课程有值;不要用它判断是否完成。
{
"id": 1001,
"name": "作业 1",
"description": "<p>要求...</p>",
"due_at": "2026-10-01T15:59:59Z",
"unlock_at": null,
"lock_at": null,
"points_possible": 100,
"submission_types": ["online_upload"],
"assignment_group_id": 12,
"html_url": "https://oc.sjtu.edu.cn/courses/12345/assignments/1001",
"allowed_attempts": 1,
"is_quiz_assignment": false,
"published": true,
"submission": { "...": "见下" }
}-
due_at是 DDL 的唯一可靠来源。有些课全部不填 due,不能靠日历/planner 补。 -
description是 HTML,渲染前要清洗。 -
submission_types常见online_upload、none(纯成绩项)、online_quiz;online_text_entry、media_recording结构上要保留处理位,但本人数据里没出现。
{
"workflow_state": "submitted",
"score": null,
"grade": null,
"submitted_at": "2026-09-30T12:00:00Z",
"graded_at": null,
"attempt": 1,
"late": false,
"missing": false,
"seconds_late": 0,
"excused": false,
"redo_request": false,
"attachments": [],
"submission_comments": []
}判断「完成」不要只看分数:
-
submitted_at/workflow_state=submitted|pending_review:已提交。 -
workflow_state=graded且missing !== true:已批改。 -
excused=true:已豁免。 -
redo_request=true:需重新提交。 -
submission_types全是on_paper/none:线下提交,不要标成「未交」。 - 分数为 0 不代表没交;分数为空也不代表没交。
{
"id": 9001,
"uuid": "abc...",
"folder_id": 3001,
"display_name": "lecture01.pdf",
"filename": "lecture01.pdf",
"content-type": "application/pdf",
"size": 1048576,
"created_at": "...",
"updated_at": "...",
"mime_class": "pdf",
"hidden": false,
"locked": false,
"url": "/files/9001/download?download_frd=1&verifier=..."
}-
folders返回全课程扁平列表。用parent_folder_id组树;根为null,根名固定为course files。 -
files可以整课平铺,也可以按文件夹列;字段一致。 -
display_name是界面名(中文常见),filename是原始文件名。 -
thumbnail_url、media_entry_id实测基本为null。
-
type实测出现File、Discussion;代码已按Assignment、Page、SubHeader、ExternalUrl处理,但这四种未逐一实测。 -
content_id指向具体资源;page_url指向页面。 - 模块是教师组织的「课程目录」,约 1/3 的课在用;没有模块的课只有文件树。把模块当可选视图,不要当硬性层级。
Canvas 用 Link 响应头分页:
Link: <https://oc.sjtu.edu.cn/api/v1/courses?...&page=2>; rel="next",
<https://oc.sjtu.edu.cn/api/v1/courses?...&page=5>; rel="last"关键事实:
-
per_page被本实例静默限制为 50。请求 100 也只返回 50,Link头里显示per_page=50。[实测 2026-09-10] - 必须跟随
Link的rel="next",不能自己拼page。 - 分页 URL 必须校验 origin 和
/api/v1/前缀,防止被恶意响应带去别的域。 - 记录已访问 URL,防止服务端返回重复分页导致死循环。
function nextPage(linkHeader: string | null): string | null {
const next = linkHeader
?.split(',')
.find((part) => /rel="next"/.test(part))
?.match(/<([^>]+)>/)?.[1];
if (!next) return null;
const url = new URL(next, 'https://oc.sjtu.edu.cn/api/v1/');
if (url.origin !== 'https://oc.sjtu.edu.cn' || !url.pathname.startsWith('/api/v1/')) {
throw new Error('Canvas 分页地址无效');
}
return url.href;
}
async function listAll(path: string, token: string): Promise<Record<string, any>[]> {
let next: string | null = new URL(path, 'https://oc.sjtu.edu.cn/api/v1/').href;
const visited = new Set<string>();
const result: Record<string, any>[] = [];
while (next) {
if (visited.has(next)) throw new Error('Canvas 返回了重复分页');
visited.add(next);
const response = await fetch(next, { headers: { Authorization: `Bearer ${token}` } });
if (!response.ok) throw new Error(`Canvas HTTP ${response.status}`);
result.push(...((await response.json()) as Record<string, any>[]));
next = nextPage(response.headers.get('link'));
}
return result;
}-
X-Rate-Limit-Remaining: 600.0(令牌桶)+X-Request-Cost(每请求按成本扣减)。[实测] - 触发限流通常返回 429;读
Retry-After,没有就指数退避。[官方][社区] - 同步多门课时串行或小并发,不要每门课并发轰炸。
| 状态 | 可能含义 | 处理 |
|---|---|---|
401 + JSON {"status":"未授权","errors":[{"message":"用户无权执行该操作"}]}
|
可能是 Token 失效,也可能是单课无权限 | 先调 /users/self/profile 判断 Token;单课 401 跳过并标记 |
| 404 | 功能未开/资源不存在 | 该分支跳过,不要当成整站失败 |
| 429 | 限流 | 退避重试 |
| 5xx | 服务端临时错误 | 重试一次,仍失败则报错 |
实测:/courses 返回 48 门课,其中 5 门列表可见、详情 401。Token 本身仍然有效。[实测 2026-09-10]
这是 Canvas 最容易写错的部分。
GET /api/v1/files/{fid} Bearer Token
→ metadata.url = /files/{fid}/download?download_frd=1&verifier=<uuid>
GET metadata.url Bearer Token, redirect: manual
→ 302 Location: https://s3.jcloud.sjtu.edu.cn/...?X-Amz-Expires=3600
GET Location 不要带 Authorization
→ 200 文件流;支持 Range
关键点:
-
用
redirect: 'manual'拿Location。这样签名 URL 不经过 fetch 的自动重定向、不进入渲染层,也方便流式/重试。不要依赖不同运行时对跨源重定向的Authorization处理。不同运行时的实现并不一致。 - 第二段请求不带 Canvas Token。预签名 URL 自己就是凭据。
- 预签名 URL 有效期 1 小时(
X-Amz-Expires=3600),不要存库、不要落日志、不要发给模型。 -
verifier下载 URL 长期稳定,可以缓存;S3 链接不能缓存。 - 单文件实测最大约 96 MB(教材 PDF);下载后校验
Content-Length/size。 - 大文件下载放主进程/后台,不要阻塞 UI;并发下载要限速。
- 文件名冲突要处理;Windows 下还要过滤非法字符和保留名。
import { writeFile } from 'node:fs/promises';
async function downloadCanvasFile(fileId: number, token: string, target: string) {
const meta = await fetch(`https://oc.sjtu.edu.cn/api/v1/files/${fileId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!meta.ok) throw new Error(`metadata ${meta.status}`);
const info = (await meta.json()) as { url: string; size: number; 'content-type': string };
const first = await fetch(new URL(info.url, 'https://oc.sjtu.edu.cn'), {
headers: { Authorization: `Bearer ${token}` },
redirect: 'manual',
});
if (first.status < 300 || first.status >= 400) {
throw new Error(`expected redirect, got ${first.status}`);
}
const location = first.headers.get('location');
if (!location) throw new Error('missing Location');
const signed = new URL(location);
if (signed.protocol !== 'https:' || signed.hostname !== 's3.jcloud.sjtu.edu.cn') {
throw new Error('unexpected download host');
}
const file = await fetch(signed, { redirect: 'error' });
if (!file.ok) throw new Error(`download ${file.status}`);
const bytes = new Uint8Array(await file.arrayBuffer());
if (info.size && bytes.length !== info.size) throw new Error('size mismatch');
await writeFile(target, bytes);
}生产代码应流式写入临时文件,校验后再 rename。上面的示例只展示协议形状,没有处理大文件内存占用。
个人 Token 默认带写入 scope,但只验证了很少的写端点:
-
讨论回复可用:
POST /courses/:id/discussion_topics/:topic_id/entries,JSON body{"message":"..."},Content-Type: application/json; charset=utf-8,返回 201。学生回复公告也可以。[实测 2026-09-15] - 作业提交、上传文件、改成绩等未验证。不要因为「scope 允许」就假设端点可用。
- 写操作要有用户确认;不要在同步/后台任务里偷偷写。
-
per_page超过 50 无效:必须跟Link,不能信任请求参数。 - 401 不一定是 Token 失效:单课 401 可能只是无权限;先验证 profile。
-
历史课长期
available:按term分类,不要按workflow_state。 -
planner/todo/upcoming_events/calendar_events基本为空:交大教师不用这些功能;DDL 要自己遍历assignments.due_at聚合。[实测 2026-09-10/15] -
跨课程公告必须传
context_codes[]:不传会 400Missing context_codes;active_only=true还要日期。 -
文件夹是扁平列表:必须按
parent_folder_id组树。 -
模块和文件会指向同一个资源:按
source(file id / assignment id / discussion id / page url)去重,不要重复展示。 -
course_code是复合字符串:解析课程号/班号/学期时写健壮的回退,不要按固定分隔符硬切。 -
due_at是唯一 DDL:没有 due 的作业就是没有 DDL,不要用课程结束时间或考试时间猜。 - 文件下载 302 到 S3:第二段不能带 Authorization,预签名 URL 是凭据。
- 大文件:教材 PDF 可到 96 MB;要流式、校验、限速。
-
quizzes可能返回 404:功能未开启,不要视为整站错误。 -
activity_stream收录时效不明:不要拿它做实时通知。
curl 'https://oc.sjtu.edu.cn/api/v1/courses?include[]=term&include[]=enrollments&per_page=50' \
-H "Authorization: Bearer $CANVAS_TOKEN"curl 'https://oc.sjtu.edu.cn/api/v1/courses/12345/assignments?include[]=submission&per_page=50' \
-H "Authorization: Bearer $CANVAS_TOKEN"const token = process.env.CANVAS_TOKEN!;
const courses = await listAll('/courses?include[]=term&per_page=50', token);
const latestTerm = [...new Set(courses.map((c) => c.term?.name).filter(Boolean))]
.sort()
.at(-1);
const current = courses.filter((c) => c.term?.name === latestTerm);更完整的下载器见课程资料下载器。
-
不要依赖
planner/todo/calendar_events做 DDL 聚合:交大实例基本为空。用作业due_at。 -
不要用
workflow_state判断学期:历史课仍然是available。 -
不要用
per_page控制页大小:本实例上限 50。 -
不要把 Canvas 当课堂录像来源:录像在
v.sjtu.edu.cn,Canvas 里只是 LTI 入口。
- 课程/作业/提交/文件/模块/讨论字段与行为:
[实测 2026-09-10、2026-09-15,本人账号,5 学期 48 门课] -
per_page=50、401 的两种含义、S3 302 与 1 小时签名、96MB:[实测 2026-09-10] - 讨论回复 201:
[实测 2026-09-15] - Canvas REST API 上游文档:
[官方 2026-10-25]https://canvas.instructure.com/doc/api/ - 交大实例文档:
[官方 2026-10-25]https://oc.sjtu.edu.cn/doc/api/index.html
- 2026-10-25:首版。基于
docs/sjtu-apis.md的 Canvas 实测记录重写,补齐端点表、字段语义、下载链路和常见问题。
本 Wiki 记录的是开发实践,不是学校官方承诺。每个结论都标注证据与日期;校园平台会调整字段、权限和登录方式,动手前请按页面里的「最后验证」复核。发现过期内容请提 Issue 或直接改。