#!/usr/bin/env python3 """ 莞工教务系统 CLI - 直接通过API操作选课 ====================================== 依赖: browser-act (已安装), Python 3.x 使用前需要已在 browser-act 中登录教务系统并保持会话活跃。 用法: python3 dgut.py list-cycles # 列出可选课的轮次 python3 dgut.py list-tabs # 探测选课界面开放了哪些标签页 python3 dgut.py list-courses [标签序号] # 列出课程 (默认第1个有API的标签) python3 dgut.py select # 选课 python3 dgut.py drop # 退课 python3 dgut.py search <关键词> # 搜索课程 """ import argparse import sys import textwrap from typing import Optional # 将 dgut-gui 目录加入 path 以导入 dgut_core import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "dgut-gui")) from dgut_core import ( ORIGIN, discover_tabs, drop_course, ensure_in_course_selection, ensure_session, fetch_api, get_courses, get_cycles, select_course, ) # ─── 全局状态 ──────────────────────────────────────────────────────────── _current_tab: Optional[dict] = None _tabs: list[dict] = [] _course_cache: list[dict] = [] # ─── 命令实现 ──────────────────────────────────────────────────────────── def cmd_list_cycles(): cycles = get_cycles() if not cycles: print("📭 当前没有可选的选课轮次。") return print(f"\n{'='*60}") print(f" 📚 可选选课轮次 ({len(cycles)} 个)") print(f"{'='*60}") for i, cyc in enumerate(cycles, 1): print(f"\n [{i}] {cyc.get('xklc_mc', '未知')}") print(f" 学期: {cyc.get('xnxq01id', '-')}") print(f" 时间: {cyc.get('xksj', '-')}") print(f" 状态: {'进行中 ✅' if cyc.get('xkzt') == '1' else '未开始 ⏳'}") print(f" 选退: {cyc.get('txkzmc', '-')}") print(f" jx0502zbid: {cyc.get('jx0502zbid', '-')}") def cmd_list_tabs(): global _tabs print("🔄 进入选课界面...") if not ensure_in_course_selection(): print("⚠️ 无法进入选课界面,请检查浏览器状态。") return cycles = get_cycles() if not cycles: print("📭 没有选课轮次,无法探测标签页。") return # 用第一个进行中的轮次 cyc = next((c for c in cycles if c.get("xkzt") == "1"), cycles[0]) jx0502zbid = cyc.get("jx0502zbid", "") if not jx0502zbid: print("❌ 无法获取轮次ID。") return print(f"\n📋 选课轮次: {cyc.get('xklc_mc', '-')}") print(f"{'='*60}") _tabs = discover_tabs(jx0502zbid) if not _tabs: print("⚠️ 未探测到任何标签页。") return print(f"\n 共发现 {len(_tabs)} 个标签页:\n") for i, t in enumerate(_tabs, 1): api_tag = " ✅ 有课程数据" if t.get("api") else " 📄 信息页" print(f" [{i}] {t['label']}{api_tag}") print(f" ID: {t['id']} | API: {t.get('api', '无')}") return _tabs def cmd_list_courses(tab_index: Optional[int] = None): global _tabs, _course_cache, _current_tab if not _tabs: print("🔄 首次使用,先探测标签页...") result = cmd_list_tabs() if not result: return # 找有API的标签 course_tabs = [(i, t) for i, t in enumerate(_tabs) if t.get("api")] if not course_tabs: print("⚠️ 没有可加载课程数据的标签页。") return if tab_index is not None: idx = tab_index - 1 if idx < 0 or idx >= len(_tabs) or not _tabs[idx].get("api"): print(f"❌ 标签 [{tab_index}] 无效或无课程数据。") return chosen = _tabs[idx] else: chosen = course_tabs[0][1] _current_tab = chosen print(f"\n📋 加载: {chosen['label']}") print(f"{'='*60}") referer = f"{ORIGIN}{chosen['iframe']}" courses = get_courses(chosen["api"], referer) _course_cache = courses if not courses: print("📭 无课程数据。") return selected = [c for c in courses if c.get("xkzt") == "9"] sel_credits = sum(float(c.get("xf", 0)) for c in selected) print(f"\n 共 {len(courses)} 门课程,已选 {len(selected)} 门 ({sel_credits:.1f}学分)\n") for i, c in enumerate(courses, 1): xkzt = c.get("xkzt", "0") status = "✅ 已选" if xkzt == "9" else "⬜ 可选" action = "[退课]" if xkzt == "9" else "[选课]" print(f" [{i:2d}] {c.get('kcmc', '-'):20s} {c.get('kch', '-'):8s} " f"{str(c.get('xf', '-')):4s}学分 余{c.get('syrs', '?'):>3s}座 {status} {action}") def cmd_select(kcid_or_name: str): global _course_cache, _current_tab if not _course_cache: print("🔄 先加载课程列表...") cmd_list_courses() if not _current_tab: print("❌ 未选择课程类型。先执行 list-courses。") return course = _find_course(kcid_or_name) if not course: print(f"❌ 未找到 '{kcid_or_name}'") return if course.get("xkzt") == "9": print(f"⚠️ '{course['kcmc']}' 已选,无需重复选课。") return referer = f"{ORIGIN}{_current_tab['iframe']}" result = select_course(course["kch"], course["jx0404id"], referer) if result.get("success"): print(f"✅ 选课成功: {course['kcmc']} ({result.get('message', '')})") else: print(f"❌ 选课失败: {result.get('message', '未知错误')}") def cmd_drop(kcid_or_name: str): global _course_cache, _current_tab if not _course_cache: print("🔄 先加载课程列表...") cmd_list_courses() if not _current_tab: print("❌ 未选择课程类型。先执行 list-courses。") return course = _find_course(kcid_or_name) if not course: print(f"❌ 未找到 '{kcid_or_name}'") return if course.get("xkzt") != "9": print(f"⚠️ '{course['kcmc']}' 未选,无需退课。") return referer = f"{ORIGIN}{_current_tab['iframe']}" result = drop_course(course["jx0404id"], referer) if result.get("success"): print(f"✅ 退课成功: {course['kcmc']}") else: print(f"❌ 退课失败: {result.get('message', '未知错误')}") def cmd_search(keyword: str): global _course_cache if not _course_cache: print("🔄 先加载课程列表...") cmd_list_courses() kw = keyword.lower() results = [c for c in _course_cache if kw in c.get("kcmc", "").lower() or kw in c.get("kch", "").lower()] if not results: print(f"📭 未找到包含 '{keyword}' 的课程。") return print(f"\n 🔍 找到 {len(results)} 门课程:\n") for c in results: status = "✅ 已选" if c.get("xkzt") == "9" else "⬜ 可选" print(f" {c.get('kcmc', '-'):20s} {c.get('kch', '-'):8s} " f"{str(c.get('xf', '-')):4s}学分 {status}") def _find_course(kcid_or_name: str) -> Optional[dict]: for c in _course_cache: if c.get("kch") == kcid_or_name or c.get("kcmc") == kcid_or_name: return c return None # ─── CLI ───────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="莞工教务系统 CLI - 选课工具(自适应选课类型)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent(""" 示例: python3 dgut.py list-cycles # 查看可选课轮次 python3 dgut.py list-tabs # 探测选课类型标签 python3 dgut.py list-courses # 默认加载第1个有数据的标签 python3 dgut.py list-courses 2 # 加载第2个标签 python3 dgut.py select 0410578 # 按编号选课 python3 dgut.py select "微信小程序" # 按名称选课 python3 dgut.py drop 0410531 # 退课 python3 dgut.py search 鸿蒙 # 搜索课程 """), ) parser.add_argument("command", nargs="?", choices=["list-cycles", "list-tabs", "list-courses", "select", "drop", "search"], help="操作命令") parser.add_argument("arg", nargs="?", default="", help="命令参数") args = parser.parse_args() if not args.command: parser.print_help() return if not ensure_session(): print(f"⚠️ 会话 '{'jwx-s1'}' 未找到。请先通过 browser-act 登录教务系统。") sys.exit(1) try: if args.command == "list-cycles": cmd_list_cycles() elif args.command == "list-tabs": cmd_list_tabs() elif args.command == "list-courses": idx = int(args.arg) if args.arg and args.arg.isdigit() else None cmd_list_courses(idx) elif args.command == "select": if not args.arg: print("❌ 请指定课程编号或名称") return cmd_select(args.arg) elif args.command == "drop": if not args.arg: print("❌ 请指定课程编号或名称") return cmd_drop(args.arg) elif args.command == "search": if not args.arg: print("❌ 请指定搜索关键词") return cmd_search(args.arg) except RuntimeError as e: print(f"\n❌ 错误: {e}") sys.exit(1) if __name__ == "__main__": main()