莞工教务选课系统 CLI + GUI
通过 browser-act 浏览器会话直接调用教务系统 API, 支持命令行和图形界面操作选课/退课,自适应不同选课轮次。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
290
dgut.py
Executable file
290
dgut.py
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/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 <kcid/名称> # 选课
|
||||
python3 dgut.py drop <kcid/名称> # 退课
|
||||
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()
|
||||
Reference in New Issue
Block a user