1. 项目概述
如果你是一名在校学生,或者像我一样,经常需要和学校的在线学习平台(LMS)打交道,那么你肯定对D2L Brightspace这个界面不陌生。每天登录网页,在课程、作业、成绩、公告之间来回切换,手动整理截止日期,或者只是想快速看一眼某门课的成绩构成,这些操作虽然基础,但重复且琐碎。更别提当你正在用AI编程助手(比如Claude Code、Cursor)写代码或者规划学习时,突然需要查一下作业要求或者成绩,还得切出编辑器,打开浏览器,登录,再一层层点进去——这个过程足以打断任何流畅的思绪。
d2l-cli这个工具,就是为了解决这个“最后一公里”的痛点而生的。它是一个纯粹的命令行工具,核心目标就一个:让你能用最直接的方式,从D2L Brightspace里“只读”地拉取所有你需要的信息。成绩、作业、课程内容、教学大纲、公告,甚至是下载课件和作业附件,所有这些操作,现在只需要在终端里敲一行命令就能完成。更重要的是,它的设计哲学是“AI Agent优先”,这意味着它的输出格式(尤其是--md标记)是专门为像Claude Code这样的AI编程助手“喂食”而优化的,让AI能无缝理解并处理你的学业数据,从而帮你自动化更多事情。
我自己作为开发者兼学生,在几个学期的“手动劳动”后,终于受不了了,于是动手写了这个工具。它不改变D2L上的任何数据(严格只读),只充当一个高效、可脚本化的数据管道。下面,我就来详细拆解一下这个工具的设计思路、核心实现,以及如何把它深度集成到你的学习和开发工作流中。
2. 核心设计思路与架构解析
2.1 为什么是“只读”CLI?
首先必须明确d2l-cli的定位:它是一个信息查询与获取工具,而非交互式客户端。选择“只读”作为核心设计原则,是基于以下几点考量:
安全边界清晰:教育系统的数据敏感且重要。任何涉及“写”操作(提交作业、发布帖子、修改成绩)的功能,都伴随着复杂的业务逻辑、严格的权限校验和不可逆的操作风险。将工具限定在“只读”范畴,相当于画下了一条绝对的安全红线,从根本上避免了误操作导致数据污染或违反学术规定的可能性。这对于一个旨在提高效率的个人工具来说,是至关重要的自律。
实现复杂度与维护成本:D2L Brightspace的API虽然提供了读写接口,但“写”操作的API通常更复杂,需要处理表单数据、文件上传、状态转换等,且不同学校、不同课程对同一操作(如作业提交)的实现细节可能不同。保持“只读”可以让我们专注于最通用、最稳定的数据获取逻辑,大大降低代码复杂度和后续的维护负担。
与AI Agent协作的契合度:当前AI助手(尤其是编程类Agent)最擅长的是信息处理、分析和基于现有信息的决策建议,而非在复杂、多步骤的Web表单中执行操作。一个稳定的、结构化的数据源,正是AI发挥价值的最佳土壤。
d2l-cli负责把非结构化的网页信息变成结构化的数据,AI负责分析和给出建议,分工明确,效率最高。
2.2 面向AI Agent的接口设计
这是d2l-cli区别于其他类似脚本工具的关键。传统的CLI工具输出主要面向人类阅读,而d2l-cli需要同时服务两种“用户”:终端前的人类,和调用它的AI程序。
三重输出格式:
- 默认表格格式 (
human-readable):使用像rich或tabulate这样的库生成对齐美观的ASCII表格,方便人类在终端快速浏览。例如,d2l grades会输出一个带有课程名称、成绩项、得分、权重和计算后总分的清晰表格。 - JSON格式 (
--json):这是为其他脚本、程序或需要进一步处理数据的场景准备的。输出是标准的、可解析的JSON对象,包含了所有原始字段,如ID、时间戳、原始分数等。你可以用jq这样的工具进行管道处理,或者集成到自己的自动化脚本里。 - Markdown格式 (
--md):这是为AI Agent量身定做的。AI模型对Markdown格式的解析和理解能力通常很强。--md格式的输出会包含完整的上下文信息(比如将课程ID和名称一起列出),使用ISO标准的日期时间格式(如2023-10-27T23:59:00Z),并且文字描述更连贯。当Claude Code读取到这样的输出时,它能更准确地理解“下周一有什么作业”或“我数据结构这门课目前平均分是多少”。
- 默认表格格式 (
命令的语义化与模糊匹配:为了让AI(以及用户)能用自然语言的方式调用,命令参数设计得非常灵活。例如,
d2l grades "data structures"中的"data structures"并不需要是完整的课程名。工具内部会进行模糊匹配,在用户已注册的课程列表中,寻找名称或代码中包含该关键词的课程。这模仿了人类“大概记得名字”的查询习惯,降低了使用门槛。
2.3 认证策略:平衡便捷与安全
D2L Brightspace 通常使用OAuth 2.0或类似的Bearer Token(JWT)进行API认证。这个Token有效期很短(通常1小时),但获取它需要经过学校统一的SSO(单点登录)流程,这给自动化带来了挑战。
d2l-cli提供了两种认证方式,覆盖了从便捷到全自动的不同场景:
浏览器捕获模式 (
d2l login):这是最推荐给个人用户的方式。工具会利用playwright无头浏览器自动化库,模拟用户打开浏览器、跳转到学校登录页、输入凭据(或利用已有的SSO会话Cookie)、登录D2L、并最终从网络请求中“捕获”那个宝贵的Bearer Token。这个过程对用户来说是“一键完成”的,体验最好。它依赖于浏览器环境中可能已经存在的持久化会话Cookie,这意味着如果你最近在浏览器里登录过D2L,运行d2l login可能连密码都不用输。无头刷新模式 (
d2l login --headless):这是为服务器环境或需要定期自动刷新的场景设计的。在首次通过浏览器模式登录并保存了浏览器上下文(Profile)后,后续可以运行此命令。它会在后台(无图形界面)启动浏览器,使用之前保存的Cookie尝试静默登录并获取新Token。结合cron定时任务,可以实现Token的自动续期,保证CLI在服务器上7x24小时可用。手动Token模式:作为保底方案,也提供了手动从浏览器开发者工具中复制Token,并按照固定格式存入配置文件的方法。这适合在无法安装浏览器自动化环境或调试时使用。
实操心得:关于Token持久化将Token明文存储在
~/.d2l/token.json中会带来安全风险。在生产环境或多人使用的机器上,建议对该文件设置严格的权限(如chmod 600 ~/.d2l/token.json)。更进阶的做法是,可以修改config.py,将Token存储在系统的密钥管理服务(如macOS的Keychain、Linux的pass)中,但这会增加部署复杂性。对于个人电脑上的使用,文件权限控制通常已足够。
3. 核心功能模块深度拆解
3.1 课程与身份信息获取
这是所有功能的基石。D2L API 提供了/d2l/api/lp/(version)/enrollments/myenrollments/之类的端点来获取当前用户注册的所有课程。
# 伪代码逻辑示意 def get_courses(self, all_terms=False): """获取课程列表""" # 调用API,获取原始JSON数据 raw_data = self._api_get("enrollments/myenrollments/") courses = [] for item in raw_data["Items"]: org_unit = item["OrgUnit"] course = { "id": org_unit["Id"], "code": org_unit["Code"], # 如 "CS-301" "name": org_unit["Name"], # 如 "Data Structures and Algorithms" "start_date": org_unit["StartDate"], "end_date": org_unit["EndDate"], "is_active": _is_current_term(org_unit), # 判断是否当前学期 } courses.append(course) # 根据 all_terms 参数过滤 if not all_terms: courses = [c for c in courses if c["is_active"]] return courses关键点:
- 缓存策略:课程列表不会频繁变动,因此可以在内存或磁盘中进行短期缓存(例如5-10分钟),避免每次执行命令都发起API请求,提升响应速度。
- 模糊匹配实现:当用户传入
"data structures"时,匹配逻辑可能综合比较课程名 (name)、课程代码 (code) 与输入关键词的相似度(可用difflib或fuzzywuzzy库),返回匹配度最高的课程对象及其ID,供后续命令使用。
3.2 成绩与学业数据抓取
成绩是学生最关心的数据之一。D2L的成绩簿API通常路径如/d2l/api/le/(version)/(orgUnitId)/grades/values/myGradeValues/。
def get_grades(self, course_id, final_only=False): """获取指定课程的成绩详情""" # 获取成绩项(作业、测验、考试等)定义 grade_objects = self._api_get(f"{course_id}/grades/") # 获取我的成绩值 my_values = self._api_get(f"{course_id}/grades/values/myGradeValues/") # 将成绩值与定义关联 gradebook = [] for go in grade_objects: grade_item = { "name": go["Name"], "max_points": go["MaxPoints"], "weight": go.get("Weight", 0), # 可能有权重 "category": go.get("CategoryName", ""), } # 查找对应的成绩 for mv in my_values: if mv["GradeObjectId"] == go["Id"]: grade_item["score"] = mv["PointsNumerator"] grade_item["grade"] = mv.get("DisplayedGrade", "") grade_item["is_released"] = mv.get("IsReleased", False) break gradebook.append(grade_item) # 计算当前总分(基于已发布的成绩) if not final_only: calculated_grade = self._calculate_current_grade(gradebook) else: # 可能调用另一个API端点获取教师发布的最终成绩 calculated_grade = self._api_get(f"{course_id}/grades/final/") return {"gradebook": gradebook, "calculated_grade": calculated_grade}注意事项:
- 成绩计算逻辑差异:不同教授设置成绩计算方式(是否去掉最低分、权重如何分配、额外加分等)千差万别。
d2l-cli计算出的“当前总分”仅供参考,最准确的永远是教师发布的官方成绩。工具应明确提示这一点。 - 数据新鲜度:成绩不是实时更新的。工具获取的是最后一次从D2L同步到API的数据,可能与网页版有几分钟延迟。
3.3 内容与文件下载
这是另一个高频需求。D2L的课程内容通常以“模块-主题”的树形结构组织。对应的API端点可能像/d2l/api/le/(version)/(orgUnitId)/content/tree/。
def get_content_tree(self, course_id): """获取课程内容目录结构""" tree_data = self._api_get(f"{course_id}/content/tree/") # 需要递归解析这个树结构,转换为扁平的、带缩进标识的列表或嵌套的字典/对象 return self._parse_content_tree(tree_data) def download_content(self, course_id, module_identifier, output_dir="."): """下载指定模块或主题下的所有文件""" # 1. 解析模块标识符(可能是名称或ID),找到对应的模块对象 target_module = self._find_module(course_id, module_identifier) if not target_module: raise ValueError(f"Module '{module_identifier}' not found.") # 2. 获取该模块下的所有主题(Topics) topics = self._get_module_topics(course_id, target_module["id"]) # 3. 遍历主题,筛选出类型为“文件”的主题,并获取其文件下载URL for topic in topics: if topic["type"] == "file": file_url = topic["file_url"] file_name = topic["title"] or topic["file_name"] # 4. 下载文件 self._download_file(file_url, os.path.join(output_dir, file_name))实操要点:
- 路径安全:下载文件名可能包含特殊字符或路径分隔符(如
/,\),在保存到本地前必须进行清洗,防止路径遍历漏洞。 - 增量下载:可以设计一个简单的机制,记录已下载文件的哈希值或最后修改时间,避免重复下载相同文件。
- 速率限制:大量下载时,应注意在请求间添加短暂延迟,避免对D2L服务器造成压力,也防止自己的IP被临时限制。
3.4 教学大纲(Syllabus)集成
许多学校使用“SimpleSyllabus”等第三方服务来托管教学大纲。d2l-cli的创新之处在于直接集成了对此类服务的读取。
# 在 syllabus.py 中 def fetch_syllabus(course_code, school_host="kennesaw.simplesyllabus.com"): """从SimpleSyllabus获取教学大纲PDF或HTML""" # 1. 搜索API:通过课程代码找到大纲ID search_url = f"https://{school_host}/api2/syllabus-search" params = {"q": course_code} search_results = requests.get(search_url, params=params).json() if not search_results.get("results"): return None syllabus_id = search_results["results"][0]["id"] # 2. 获取API:通过大纲ID获取完整内容 fetch_url = f"https://{school_host}/api2/doc-full-page-get" data = {"id": syllabus_id} syllabus_data = requests.post(fetch_url, json=data).json() # 3. 解析并返回结构化信息(评分政策、考试日期、联系方式等) parsed_info = parse_syllabus_html(syllabus_data["html"]) return parsed_info为什么这样做?因为D2L内置的“教学大纲”工具可能只提供一个静态HTML页面或链接,而SimpleSyllabus的API提供了结构更清晰的数据。直接对接源头,能提取出更规整的“评分权重”、“考勤政策”等信息,这对于AI进行学业规划尤其有用。
4. 与AI编程助手的深度集成实战
4.1 为Claude Code创建技能(Skill)
以Claude Code为例,它支持自定义技能(Skills)。d2l-cli项目中的.claude/skills/d2l/SKILL.md文件就是一个标准的技能描述文件。
# d2l-cli Skill for Claude Code ## Description This skill allows Claude Code to interact with your D2L Brightspace learning management system to fetch academic data. ## Commands Claude can run the following commands on your behalf: - `d2l courses` - List your enrolled courses. - `d2l grades [course_name]` - Get grades for a specific course or all courses. - `d2l due [--days N]` - List assignments due in the next N days (default 7). - `d2l syllabus <course_name>` - Fetch the syllabus for a course. - `d2l download <course_name> <assignment_name> -o <dir>` - Download assignment files. - `d2l --md dump` - Get a comprehensive markdown snapshot of all your academic data. ## Examples When you ask: - "How am I doing in my Computer Science courses?" Claude will run: `d2l --md grades` and analyze the output. - "What do I have due tomorrow?" Claude will run: `d2l due --days 1` and list the items. - "Download the latest lecture slides for Calculus." Claude will first find the course ID with `d2l courses`, then run something like `d2l download-content "Calculus I" "Week 5 Derivatives" -o ./calc-notes`当Claude Code加载这个技能后,它就能理解d2l命令的上下文,并在你提出相关问题时,主动建议或直接执行这些命令来获取信息,然后基于返回的结构化数据(Markdown格式)给你生成总结、提醒或建议。
4.2 设计高效的AI提示词
为了让AI更好地利用d2l-cli,你可以在提问时采用更“指挥”式的提示词:
- 低效提示:“我这周忙吗?”
- 高效提示:“请运行
d2l due --days 7 --md,获取我未来一周的所有截止任务,然后按截止日期和课程分类,为我生成一个优先级列表,并估算每项任务可能需要的时间。”
后一种提示词明确了工具调用(d2l命令)、期望的输出格式(--md)、以及后续的数据处理任务(分类、排序、估算)。AI会先执行命令获取数据,再基于数据执行分析任务。
4.3 构建自动化学习看板
你可以将d2l-cli与cron、简单的Shell脚本或Python脚本结合,打造个人自动化系统。
示例:每日学业简报脚本 (daily_digest.py)
#!/usr/bin/env python3 import subprocess import json from datetime import datetime, timedelta import smtplib from email.mime.text import MIMEText def run_d2l_command(cmd): """执行d2l命令并返回JSON结果""" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if result.returncode == 0: return json.loads(result.stdout) else: print(f"Command failed: {result.stderr}") return None def main(): # 1. 获取未来3天的作业 due_soon = run_d2l_command('d2l --json due --days 3') # 2. 获取所有课程的最新公告(24小时内) recent_news = run_d2l_command('d2l --json news --since 24') # 3. 获取是否有未读的讨论区更新 unread_updates = run_d2l_command('d2l --json updates') # 4. 格式化报告 report = f""" # 学业日报 {datetime.now().strftime('%Y-%m-%d')} ## 即将截止的作业(未来3天) {format_due_items(due_soon)} ## 最新课程公告 {format_news(recent_news)} ## 待处理事项 - 未读讨论更新: {unread_updates.get('total_unread', 0)} 条 """ # 5. 发送邮件或保存为文件 send_email_report(report, "your-email@example.com") with open(f"/path/to/digest/daily_{datetime.now().date()}.md", "w") as f: f.write(report) if __name__ == "__main__": main()然后,在crontab中添加一行,让这个脚本每天早晨8点运行:
0 8 * * * cd /path/to/your/script && /usr/bin/python3 daily_digest.py这样,你每天起床就能在邮箱或指定文件夹里收到一份自动生成的学业简报。
5. 部署、配置与故障排查指南
5.1 分步安装与配置
假设你是在一台全新的Linux/macOS系统上部署:
基础环境准备:
# 1. 克隆仓库 git clone https://github.com/Aaryan-Kapoor/d2l-cli.git cd d2l-cli # 2. 创建并激活虚拟环境(强烈推荐,避免污染系统Python) python3 -m venv .venv source .venv/bin/activate # Linux/macOS # 在Windows上: .venv\Scripts\activate # 3. 安装基础包 pip install -e .浏览器自动化支持(用于自动登录):
# 安装带[login]额外依赖的版本,这会包含playwright pip install -e ".[login]" # 安装Chromium浏览器(用于无头操作) playwright install chromium注意:
playwright install会下载一个独立的Chromium,体积较大(约100MB),但保证了环境一致性。关键配置: 编辑
src/d2l/config.py,这是核心配置文件。# 你的学校D2L主页地址,通常是 https://[学校缩写].brightspace.com 或类似 LMS_HOST = "https://your-university.brightspace.com" # Tenant ID,这需要一点技巧获取 # 方法:登录D2L网页版,打开浏览器开发者工具(F12),切换到Network(网络)标签。 # 刷新页面,找到一个指向 `*.api.brightspace.com` 的请求。 # 在请求头(Request Headers)或响应头(Response Headers)里寻找 `x-kiss-tenant` 或类似字段,其值就是Tenant ID。 # 也可能藏在登录跳转的URL参数中。 TENANT_ID = "your_tenant_id_here" # 如果你的学校使用SimpleSyllabus,并且你想用 `d2l syllabus` 命令 # 需要找到你们学校的SimpleSyllabus域名,通常类似 `university.simplesyllabus.com` # 修改 src/d2l/commands/syllabus.py 中的对应变量。首次登录与Token获取:
# 运行登录命令,这会打开浏览器 d2l login按照浏览器提示完成学校的SSO登录流程。成功后,CLI会自动捕获Token并保存到
~/.d2l/token.json。之后在Token过期前(约1小时),所有命令都无需再认证。
5.2 服务器端无头部署
如果你想在远程服务器(如VPS)或总是开机的树莓派上运行定时任务:
- 初始有头环境配置:首先在一台有图形界面的电脑(如你的笔记本)上完成上述步骤1-4,成功运行
d2l login并生成~/.d2l/目录。 - 传输配置文件:将整个
~/.d2l/目录(包含token.json和browser_context等)打包,上传到你的服务器对应用户的home目录下。 - 服务器环境安装:在服务器上重复步骤1-3(安装基础环境和playwright chromium)。注意,服务器可能缺少一些图形库,需要安装:
# 对于Ubuntu/Debian sudo apt-get install -y libgbm-dev libnss3 libatk-bridge2.0-0 libdrm-dev libxkbcommon-dev libasound2 # 对于CentOS/RHEL sudo yum install -y atk at-spi2-atk cups-libs libdrm libXcomposite libXdamage libXrandr gtk3 - 测试无头登录:
如果成功,说明保存的浏览器会话(Cookie)仍然有效,工具可以自动刷新Token。d2l login --headless - 设置自动刷新:编辑crontab (
crontab -e),添加一行,每45分钟刷新一次Token(因为Token有效期约1小时)。*/45 * * * * cd /path/to/d2l-cli && /path/to/.venv/bin/d2l login --headless > /tmp/d2l-refresh.log 2>&1
5.3 常见问题与排查技巧
问题1:运行d2l login时报错,提示浏览器相关错误。
- 可能原因:Playwright的浏览器未正确安装,或缺少系统依赖。
- 解决:
- 确保已运行
playwright install chromium。 - 查看完整错误信息,根据提示安装缺失的系统包(如上文所述)。
- 尝试指定使用已安装的系统Chrome(如果存在):可以修改代码中Playwright的启动参数,但兼容性可能不佳。推荐使用Playwright自带的Chromium。
- 确保已运行
问题2:命令执行返回401 Unauthorized或403 Forbidden。
- 可能原因:Token已过期或无效;配置的
TENANT_ID或LMS_HOST不正确;账号权限不足。 - 解决:
- 运行
d2l token检查Token状态。如果过期,重新运行d2l login。 - 仔细核对
config.py中的LMS_HOST,确保是API的根地址,而不是课程页地址。 - 确认你的账号有访问相应课程数据的权限(通常注册了课程就有)。
- 运行
问题3:d2l syllabus命令返回“未找到大纲”或报错。
- 可能原因:学校未使用SimpleSyllabus,或使用的域名不同;课程代码不匹配。
- 解决:
- 在浏览器中手动打开一门课程的教学大纲,查看其URL。如果跳转到类似
simpleyllabus.com的网站,则说明可用。 - 用浏览器开发者工具,在网络请求中查找向
simplesyllabus.com发出的API请求,从中提取出你们学校的专属域名。 - 修改
src/d2l/commands/syllabus.py中的SYLLABUS_SEARCH_URL和SYLLABUS_FULL_URL变量。
- 在浏览器中手动打开一门课程的教学大纲,查看其URL。如果跳转到类似
问题4:AI助手(如Claude Code)无法识别或执行d2l命令。
- 可能原因:AI助手的技能未正确加载;虚拟环境未激活;
d2l命令不在AI助手的PATH中。 - 解决:
- 确认你是在激活了
d2l-cli虚拟环境的终端中启动AI助手。 - 对于Claude Code,确保项目根目录下有
.claude/skills/d2l/SKILL.md文件,或者将AGENTS.md的内容提供给AI作为上下文。 - 尝试在AI助手的聊天框中手动输入
which d2l,看是否能找到命令路径。如果找不到,可能需要将虚拟环境的bin目录添加到PATH,或者使用绝对路径调用。
- 确认你是在激活了
问题5:下载文件时文件名乱码或失败。
- 可能原因:D2L返回的文件名包含特殊字符或编码问题;网络中断。
- 解决:
- 在下载函数中增加文件名清洗逻辑,移除或替换非法字符。
- 实现重试机制和更详细的错误日志,帮助定位是网络问题还是文件权限问题。
这个工具的本质,是把一个以Web为中心的、交互复杂的学习管理系统,转变为一个以数据和API为中心的、可编程的接口。它解放了你的双手和注意力,让你和你的AI助手能更专注于学习与创造本身,而不是浪费在重复的点击和查找上。从一次简单的成绩查询,到一个全自动的学业监控系统,d2l-cli提供了一个坚实而灵活的起点。