Files

291 lines
10 KiB
Python
Raw Permalink Normal View History

#!/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()