commit 4185141e7ed70e45c7c9dac1946270c66968199e Author: EchoZenith <60392923+EchoZenith@users.noreply.github.com> Date: Mon May 4 23:07:19 2026 +0800 feat: 添加超星学习通自动评教脚本及配置文件 实现超星学习通自动评教功能,支持账号密码登录和二维码登录 添加配置文件模板、依赖文件及gitignore配置 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4e5b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environment +venv/ +env/ +.venv/ +.env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Project specific - config with sensitive credentials +data/config.py +data/config.json + +# Project specific - generated QR code images +data/*.png diff --git a/chaoxing_evaluate.py b/chaoxing_evaluate.py new file mode 100644 index 0000000..c4cfc41 --- /dev/null +++ b/chaoxing_evaluate.py @@ -0,0 +1,676 @@ +import re +import json +import sys +import base64 +import os +import requests # pyright: ignore[reportMissingModuleSource] + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "data")) +import config as cfg # pyright: ignore[reportMissingImports] + +try: + from Crypto.Cipher import AES # pyright: ignore[reportMissingImports] +except ImportError: + AES = None + + +AES_KEY = "u2oh6Vu^HWe4_AES" +LOGIN_PAGE = "https://passport2.chaoxing.com/login?fid=&newversion=true&refer=https%3A%2F%2Fi.chaoxing.com" + + +def aes_encrypt(text: str) -> str: + key = AES_KEY.encode("utf-8") + iv = key + raw = text.encode("utf-8") + pad_len = AES.block_size - len(raw) % AES.block_size + raw += bytes([pad_len] * pad_len) + cipher = AES.new(key, AES.MODE_CBC, iv) + encrypted = cipher.encrypt(raw) + return base64.b64encode(encrypted).decode("utf-8") + + +class ChaoxingEvaluator: + def __init__(self, cookies: dict = None): + self.session = requests.Session() + if cookies: + self.session.cookies.update(cookies) + self.session.headers.update({ + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,image/apng,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + }) + self.base_url = "https://newes.chaoxing.com" + + @classmethod + def login(cls, username: str, password: str) -> "ChaoxingEvaluator": + if AES is None: + raise ImportError("请先安装 pycryptodome: pip3 install pycryptodome") + + session = requests.Session() + session.headers.update({ + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + }) + + login_page = "https://passport2.chaoxing.com/login?fid=&newversion=true&refer=https%3A%2F%2Fi.chaoxing.com" + print("访问登录页面...") + session.get(login_page) + + encrypted_uname = aes_encrypt(username) + encrypted_pwd = aes_encrypt(password) + + login_data = { + "fid": "-1", + "uname": encrypted_uname, + "password": encrypted_pwd, + "refer": "https%253A%252F%252Fi.chaoxing.com", + "t": "true", + "forbidotherlogin": "0", + "validate": "", + "doubleFactorLogin": "0", + "independentId": "0", + "independentNameId": "0", + } + + login_url = "https://passport2.chaoxing.com/fanyalogin" + headers = { + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Referer": login_page, + "Origin": "https://passport2.chaoxing.com", + } + + print("正在登录...") + resp = session.post(login_url, data=login_data, headers=headers) + result = resp.json() + + if not result.get("status"): + print(f"登录失败: {result}") + raise Exception(f"登录失败: {result}") + + print("登录成功!") + + evaluator = cls.__new__(cls) + evaluator.session = session + evaluator.base_url = "https://newes.chaoxing.com" + + cfg.save_cookies(dict(session.cookies)) + return evaluator + + @classmethod + def qr_login(cls) -> "ChaoxingEvaluator": + import time + import subprocess + + session = requests.Session() + session.headers.update({ + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + }) + + print("获取登录页面...") + resp = session.get(LOGIN_PAGE) + + uuid_match = re.search(r'id="uuid"\s*value="([^"]+)"', resp.text) or \ + re.search(r'value="([^"]+)"\s+id="uuid"', resp.text) + enc_match = re.search(r'id="enc"\s*value="([^"]+)"', resp.text) or \ + re.search(r'value="([^"]+)"\s+id="enc"', resp.text) + if not uuid_match or not enc_match: + raise Exception("无法获取二维码参数") + + uuid = uuid_match.group(1) + enc = enc_match.group(1) + + qr_url = f"https://passport2.chaoxing.com/createqr?uuid={uuid}&fid=-1" + qr_resp = session.get(qr_url, headers={"Referer": LOGIN_PAGE}) + qr_data = qr_resp.content + + qr_file = os.path.join(os.path.dirname(__file__), "data", "chaoxing_qr.png") + with open(qr_file, "wb") as f: + f.write(qr_data) + print(f"二维码已保存到: {qr_file}") + print("请使用超星学习通APP扫码登录...") + + if sys.platform == "darwin": + subprocess.Popen(["open", qr_file]) + elif sys.platform == "win32": + os.startfile(qr_file) + else: + print(f"请打开文件扫码: {qr_file}") + + auth_url = "https://passport2.chaoxing.com/getauthstatus/v2" + headers = { + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Referer": LOGIN_PAGE, + "Origin": "https://passport2.chaoxing.com", + } + + for attempt in range(180): + try: + time.sleep(2) + data = {"enc": enc, "uuid": uuid, + "doubleFactorLogin": "0", "forbidotherlogin": "0"} + poll_resp = session.post(auth_url, data=data, headers=headers) + result = poll_resp.json() + except KeyboardInterrupt: + print("\n用户中断扫码") + raise + + if result.get("status"): + print(f"扫码登录成功!") + evaluator = cls.__new__(cls) + evaluator.session = session + evaluator.base_url = "https://newes.chaoxing.com" + cfg.save_cookies(dict(session.cookies)) + return evaluator + + msg = result.get("mes", "") + if msg == "已扫描": + nickname = result.get("nickname", "") + print(f"\r手机端已扫描,请在APP上确认登录 ({nickname})", end="", flush=True) + elif msg == "验证通过": + continue + elif attempt < 3: + print(f"\r等待扫码... ({attempt + 1}/3)", end="", flush=True) + + raise Exception("二维码登录超时") + + @classmethod + def auto(cls) -> "ChaoxingEvaluator": + cookies = cfg.load_cookies() + if cookies: + evaluator = cls(cookies) + if evaluator.check_login_valid(): + print("使用已保存的Cookie登录") + return evaluator + print("Cookie已失效,重新登录") + cfg.clear_cookies() + + if cfg.QR_LOGIN: + return cls.qr_login() + + if cfg.USERNAME and cfg.PASSWORD: + try: + return cls.login(cfg.USERNAME, cfg.PASSWORD) + except Exception as e: + print(f"密码登录失败: {e}") + print("降级到二维码登录...") + return cls.qr_login() + + return cls.qr_login() + + def check_login_valid(self) -> bool: + try: + resp = self._get("/pj/semesterV2/findSemesterList", + referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj", + accept_json=True) + return resp.status_code == 200 and resp.json().get("status") == True + except Exception: + return False + + def get_cookies_dict(self) -> dict: + return dict(self.session.cookies) + + def _get(self, path: str, params: dict = None, referer: str = None, accept_json: bool = False): + url = f"{self.base_url}{path}" + headers = {} + if referer: + headers["Referer"] = referer + if accept_json: + headers["Accept"] = "application/json, text/plain, */*" + return self.session.get(url, params=params, headers=headers) + + def _post(self, path: str, data=None, params: dict = None, referer: str = None): + url = f"{self.base_url}{path}" + headers = { + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + } + if referer: + headers["Referer"] = referer + return self.session.post(url, data=data, params=params, headers=headers) + + def get_questionnaire_info(self, questionnaire_id: int) -> dict: + path = "/pj/sendClassroomLogV2/getQuestionnaire" + resp = self._get(path, params={"questionnaireId": questionnaire_id}, + referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj", + accept_json=True) + return resp.json() + + def get_evaluation_list(self, questionnaire_id: int, page: int = 1, page_size: int = 100) -> dict: + path = "/pj/newesReceptionV2/GetMyEvaluationQuestionnaireById" + params = {"questionnaireId": questionnaire_id, "pageIndex": page, "pageSize": page_size, "kw": ""} + referer = f"{self.base_url}/pj/frontv2/whatIEvaluatedDetails?_CP_=pj&questionnaireId={questionnaire_id}" + resp = self._get(path, params=params, referer=referer, accept_json=True) + return resp.json() + + def get_questionnaire_page(self, fid: int, uid: int, already_id: int, grant_id: int, + questionnaire_id: int, classification_id: str = "") -> str: + path = "/pj/newesReception/questionnaireInfo" + params = { + "fid": fid, "uId": uid, "alreadyId": already_id, "grantId": grant_id, + "questionnaireId": questionnaire_id, "type": 1, "classificationId": classification_id, + "isNewPage": "true", "source": 14, + } + referer = (f"{self.base_url}/pj/frontv2/whatIEvaluatedDetails?_CP_=pj" + f"&questionnaireId={questionnaire_id}" + f"&title=%E8%AF%84%E4%BB%B7%E8%A1%A8%E8%AF%A6%E6%83%85" + f"&evaluateObjType=1&questionnaireType=2&sendType=18" + f"&hasFinished=0&questionnaireStatus=0&showCourseProfile=0" + f"&isShowTeacherPhoto=0&alreadyType=6&classGrade=0" + f"&hideScoreLevel=0&optionrepeatcheck=0&granterType=8") + resp = self._get(path, params=params, referer=referer) + return resp.text + + @staticmethod + def parse_questions(html: str) -> list: + questions = [] + + input_pattern = re.compile( + r']*name=["\']groupTargetIds["\'][^>]*value=["\'](\d+)["\']' + ) + type_pattern = re.compile( + r']*name=["\'](\d+)_type["\'][^>]*value=["\'](\d+)["\']' + ) + maxscore_pattern = re.compile( + r']*\bid=["\'](\d+)["\'][^>]*\bmaxScore=["\']([\d.]+)["\']' + ) + minscore_pattern = re.compile( + r']*\bid=["\'](\d+)["\'][^>]*\bminScore=["\']([\d.]+)["\']' + ) + + target_ids = input_pattern.findall(html) + type_matches = {m[0]: m[1] for m in type_pattern.findall(html)} + maxscore_matches = {m[0]: float(m[1]) for m in maxscore_pattern.findall(html)} + minscore_matches = {m[0]: float(m[1]) for m in minscore_pattern.findall(html)} + + seen = set() + for tid in target_ids: + if tid in seen: + continue + seen.add(tid) + ttype = int(type_matches.get(tid, 0)) + item = {"id": tid, "type": ttype} + if ttype == 5: + item["maxScore"] = maxscore_matches.get(tid, 0) + item["minScore"] = minscore_matches.get(tid, 0) + questions.append(item) + + return questions + + def check_score_is_rightful(self, questionnaire_id: int, target_id: int, uid: int, score: float, + referer: str) -> bool: + path = "/pj/newesReception/checkScoreIsRightful" + data = {"questionnaireId": questionnaire_id, "targetId": target_id, + "uId": uid, "score": score} + resp = self._post(path, data=data, referer=referer) + result = resp.json() + return not result.get("flag", True) + + def check_no_answer_target(self, questionnaire_id: int, total_score: float, + no_answer_ids: list, referer: str) -> dict: + path = "/pj/newesReception/checkNoAnswerTarget" + data = { + "questionnaireId": questionnaire_id, + "questionnaireScore": total_score, + "noAnswerTargetIds": json.dumps(no_answer_ids), + } + resp = self._post(path, data=data, referer=referer) + return resp.json() + + def check_total_score_is_rightful(self, questionnaire_id: int, uid: int, + score: float, referer: str) -> bool: + path = "/pj/newesReception/checkTotalScoreIsRightful" + data = {"questionnaireId": questionnaire_id, "uId": uid, "score": score} + resp = self._post(path, data=data, referer=referer) + result = resp.json() + return not result.get("flag", True) + + def save_questionnaire(self, fid: int, uid: int, questionnaire_id: int, + already_id: int, grant_id: int, + questions: list, total_score: float, + referer: str) -> dict: + path = "/pj/newesReception/saveQuestionnaire" + params = { + "checkScore": total_score, + "questionnaireId": questionnaire_id, + "fid": fid, + "uId": uid, + "grantId": grant_id, + "alreadyId": already_id, + } + + form_data = [ + ("uId", str(uid)), + ("fid", str(fid)), + ("questionnaireId", str(questionnaire_id)), + ("alreadyId", str(already_id)), + ("grantId", str(grant_id)), + ] + + for q in questions: + qid_str = str(q["id"]) + form_data.append(("groupTargetIds", qid_str)) + form_data.append((f"{qid_str}_type", str(q["type"]))) + form_data.append((f"{qid_str}_chooseSetUp", "1")) + if q["type"] == 5: + score = q["maxScore"] + form_data.append((qid_str, str(score))) + elif q["type"] == 4: + form_data.append(("jumpInfo", "")) + form_data.append((qid_str, "")) + + form_data.append(("saveType", "2")) + form_data.append(("submitreasons", "")) + form_data.append(("submit_highscore_reasons", "")) + form_data.append(("submit_lowscore_reasons", "")) + form_data.append(("checkScore", str(total_score))) + + resp = self._post(path, data=form_data, params=params, referer=referer) + return resp.json() + + def evaluate_single(self, fid: int, uid: int, already_id: int, grant_id: int, + questionnaire_id: int, teacher_name: str = "", course_name: str = "") -> bool: + print(f"\n{'='*60}") + print(f"正在评教: {teacher_name} - {course_name}") + + referer = (f"{self.base_url}/pj/newesReception/questionnaireInfo" + f"?fid={fid}&uId={uid}&alreadyId={already_id}" + f"&grantId={grant_id}&questionnaireId={questionnaire_id}" + f"&type=1&classificationId=&isNewPage=true&source=14") + + html = self.get_questionnaire_page(fid, uid, already_id, grant_id, questionnaire_id) + questions = self.parse_questions(html) + + if not questions: + print("未解析到任何题目,可能HTML结构已变更") + return False + + print(f"解析到 {len(questions)} 道题目:") + total_score = 0 + has_scoring = False + for q in questions: + if q["type"] == 5: + has_scoring = True + print(f" 打分题 [{q['id']}] 满分 {q['maxScore']} -> 给满分") + total_score += q["maxScore"] + elif q["type"] == 4: + print(f" 简答题 [{q['id']}] -> 留空") + + if has_scoring and total_score == 0: + print("所有打分题满分为0,页面可能为只读状态(已评价),跳过") + return False + + print(f"总分: {total_score}") + + no_answer_ids = [str(q["id"]) for q in questions if q["type"] != 5] + + print("正在验证各题分数...") + all_score_ok = True + for q in questions: + if q["type"] == 5: + score = q["maxScore"] + ok = self.check_score_is_rightful(questionnaire_id, int(q["id"]), uid, score, referer) + if not ok: + print(f" 题目 {q['id']} 分数验证未通过") + all_score_ok = False + else: + print(f" 题目 {q['id']} 分数验证通过") + + if not all_score_ok: + print("分数验证失败,停止提交") + return False + + print("正在检查未答题目...") + no_answer_result = self.check_no_answer_target( + questionnaire_id, total_score, no_answer_ids, referer + ) + print(f" 未答检查结果: {no_answer_result}") + + print("正在验证总分...") + total_ok = self.check_total_score_is_rightful(questionnaire_id, uid, total_score, referer) + if not total_ok: + print("总分验证未通过") + return False + print(" 总分验证通过") + + print("正在提交评教...") + result = self.save_questionnaire( + fid, uid, questionnaire_id, already_id, grant_id, + questions, total_score, referer + ) + print(f"提交结果: {result}") + + if result.get("status") == 1: + print(f"评教成功: {teacher_name} - {course_name}") + return True + else: + print(f"评教失败: {result}") + return False + + def discover_questionnaires(self) -> list: + semester_resp = self._get("/pj/semesterV2/findSemesterList", + referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj", + accept_json=True) + semester_id = None + try: + semesters = semester_resp.json() + if semesters.get("status"): + for s in semesters.get("data", []): + if s.get("iscurrent") == 1: + semester_id = s.get("id") + break + if semester_id is None and semesters.get("data"): + semester_id = semesters["data"][0].get("id") + except (json.JSONDecodeError, ValueError, TypeError): + pass + + if not semester_id: + print("无法获取学期ID") + return [] + + list_resp = self._get("/pj/newesReceptionV2/GetMyEvaluationList", + params={"evaluateObjType": "", "semesterId": semester_id, + "title": "", "sort": 2, "pageIndex": 1, "pageSize": 20}, + referer=f"{self.base_url}/pj/frontv2/evaluateList/whatIEvaluated?_CP_=pj", + accept_json=True) + questionnaires = [] + try: + data = list_resp.json() + if not data.get("status") and data.get("code") != 1: + return [] + items = [] + if data.get("status"): + items = data.get("data", {}).get("list", []) + elif data.get("code") == 1: + items = data.get("data", {}).get("list", []) + + for q in items: + qid = q.get("questionnaireId") + name = q.get("title") or q.get("name", "") + if qid: + questionnaires.append({"id": int(qid), "name": name}) + except (json.JSONDecodeError, ValueError, TypeError): + pass + + return questionnaires + + def batch_evaluate(self, questionnaire_id: int, fid: int = None, uid: int = None): + info = self.get_questionnaire_info(questionnaire_id) + if info.get("status") != 1: + print("获取问卷信息失败") + return + + eval_list_resp = self.get_evaluation_list(questionnaire_id) + if not eval_list_resp.get("status"): + print("获取评教列表失败") + return + + data = eval_list_resp.get("data", {}) + total = data.get("count", 0) + items = data.get("list", []) + + print(f"共有 {total} 条待评记录") + + success = 0 + fail = 0 + for i, item in enumerate(items, 1): + obj = item.get("alreadyObject", {}) + teacher_name = obj.get("teacherName", "未知") + course_name = obj.get("courseName", "未知") + + already_id = item.get("alreadyObjectId") + grant_id = item.get("grantId") + _fid = fid or obj.get("fid") or info.get("classroomlog", {}).get("fid") + _uid = uid or int(self.session.cookies.get("_uid", 0)) + + print(f"\n[{i}/{total}] ", end="") + ok = self.evaluate_single( + fid=_fid, uid=_uid, + already_id=already_id, grant_id=grant_id, + questionnaire_id=questionnaire_id, + teacher_name=teacher_name, course_name=course_name, + ) + if ok: + success += 1 + else: + fail += 1 + + print(f"\n{'='*60}") + print(f"评教完成! 成功: {success}, 失败: {fail}") + + +def parse_cookies_from_file(filepath: str) -> dict: + cookies = {} + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + match = re.search(r"Cookie:\s*(.+)", content) + if match: + cookie_str = match.group(1) + for part in cookie_str.split(";"): + part = part.strip() + if "=" in part: + key, value = part.split("=", 1) + cookies[key.strip()] = value.strip() + return cookies + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="超星学习通自动评教脚本") + parser.add_argument("--cookie-file", "-c", type=str, + help="Cookie文件路径(从抓包数据中复制的包含Cookie的请求头文件)") + parser.add_argument("--cookie-string", "-s", type=str, + help="Cookie字符串,例如: `fid=356; _uid=123456; ...`") + parser.add_argument("--username", "-u", type=str, + help="手机号/学号(用于自动登录)") + parser.add_argument("--password", "-p", type=str, + help="密码(用于自动登录)") + parser.add_argument("--use-config", action="store_true", + help="使用config.py中的配置(自动管理Cookie)") + parser.add_argument("--questionnaire-id", "-q", type=int, default=None, + help="问卷ID,不传则自动发现") + parser.add_argument("--fid", type=int, default=None, + help="学校fid,默认从Cookie中自动获取") + parser.add_argument("--uid", type=int, default=None, + help="用户ID,默认从Cookie中自动获取") + parser.add_argument("--list", "-l", action="store_true", + help="仅列出可用问卷,不执行评教") + parser.add_argument("--no-save-cookies", action="store_true", + help="不保存Cookie到config.json") + parser.add_argument("--qr", action="store_true", + help="使用二维码登录") + + args = parser.parse_args() + + if args.qr: + evaluator = ChaoxingEvaluator.qr_login() + elif args.use_config: + evaluator = ChaoxingEvaluator.auto() + elif args.username and args.password: + evaluator = ChaoxingEvaluator.login(args.username, args.password) + if not args.no_save_cookies: + cfg.save_cookies(evaluator.get_cookies_dict()) + print("Cookie已保存到 config.json") + elif args.cookie_file: + cookies = parse_cookies_from_file(args.cookie_file) + evaluator = ChaoxingEvaluator(cookies) + elif args.cookie_string: + cookies = {} + for part in args.cookie_string.split(";"): + part = part.strip() + if "=" in part: + key, value = part.split("=", 1) + cookies[key.strip()] = value.strip() + evaluator = ChaoxingEvaluator(cookies) + else: + parser.print_help() + print("\n错误: 请使用 --qr 或 --use-config 或提供账号密码或Cookie") + return + + if args.list: + qs = evaluator.discover_questionnaires() + if not qs: + print("未发现可用问卷") + return + print("可用问卷:") + for q in qs: + print(f" ID: {q['id']} [{q['name']}]") + return + + questionnaire_id = args.questionnaire_id + if not questionnaire_id: + qs = evaluator.discover_questionnaires() + if not qs: + print("错误: 无法自动发现问卷ID,请通过 --questionnaire-id 手动指定") + return + if len(qs) == 1: + questionnaire_id = qs[0]["id"] + print(f"自动发现问卷: ID={questionnaire_id} [{qs[0]['name']}]") + else: + print("发现多个可用问卷:") + for i, q in enumerate(qs, 1): + print(f" [{i}] ID: {q['id']} [{q['name']}]") + try: + choice = int(input("请选择序号: ").strip()) + questionnaire_id = qs[choice - 1]["id"] + except (ValueError, IndexError): + print("无效选择") + return + + cookies = evaluator.session.cookies + uid = args.uid or _get_cookie_int(cookies, "_uid") + fid = args.fid or _get_cookie_int(cookies, "fid") + + if not uid: + print("错误: 无法获取用户ID,请通过 --uid 参数提供") + return + if not fid: + print("错误: 无法获取fid,请通过 --fid 参数提供") + return + + evaluator.batch_evaluate(questionnaire_id, fid=fid, uid=uid) + + +def _get_cookie_int(cookies, name): + val = cookies.get(name) + if val: + try: + return int(val) + except (ValueError, TypeError): + pass + return None + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n用户中断,已退出") + sys.exit(0) diff --git a/data/config.example.py b/data/config.example.py new file mode 100644 index 0000000..e01f48e --- /dev/null +++ b/data/config.example.py @@ -0,0 +1,34 @@ +import json +import os +import time + +CONFIG_FILE = os.path.join(os.path.dirname(__file__), "config.json") + +USERNAME = "your_username" # 替换为你的学习通用户名 +PASSWORD = "your_password" # 替换为你的学习通密码 +QR_LOGIN = False # 设为 True 启用二维码登录(优先级高于账号密码) + + +def save_cookies(cookies_dict: dict): + data = { + "cookies": cookies_dict, + "saved_at": int(time.time()), + } + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def load_cookies() -> dict | None: + if not os.path.exists(CONFIG_FILE): + return None + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("cookies") + except (json.JSONDecodeError, IOError): + return None + + +def clear_cookies(): + if os.path.exists(CONFIG_FILE): + os.remove(CONFIG_FILE) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e83338c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.28.0 +pycryptodome>=3.15.0