├── legacy ├── v3.0.1 │ ├── .gitignore │ ├── requirements.txt │ ├── screenshot.png │ ├── main.py │ ├── legacy │ │ ├── login.py │ │ └── tst_login.py │ ├── RUST_VERSION_README.txt │ ├── misc.py │ ├── verify.py │ └── main_new.py └── v2.1.0 │ ├── requirements.txt │ ├── config.json │ ├── get_config.py │ ├── parse_rollcalls.py │ ├── verify.py │ ├── main.py │ ├── main_gui.py │ └── gui.py ├── .gitignore ├── xmu-rollcall-cli ├── xmu_rollcall │ ├── __init__.py │ ├── utils.py │ ├── rollcall_handler.py │ ├── verify.py │ ├── config.py │ ├── cli.py │ └── monitor.py ├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── LICENSE ├── pyproject.toml ├── setup.py └── PYPI_UPLOAD_GUIDE.md ├── docs ├── transplant.md ├── quickstart.md ├── ChangeLog.md └── demo.md ├── Tronclass-URL-list ├── main.py └── result.csv ├── LICENSE ├── .github └── workflows │ └── build-exe.yml └── README.md /legacy/v3.0.1/.gitignore: -------------------------------------------------------------------------------- 1 | cookies.json -------------------------------------------------------------------------------- /legacy/v3.0.1/requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome 2 | xmulogin 3 | aiohttp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /.venv/ 3 | /.idea/ 4 | legacy/v3.0.1/info.txt 5 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/__init__.py: -------------------------------------------------------------------------------- 1 | """XMU Rollcall CLI Package""" 2 | 3 | __version__ = "3.1.6" 4 | 5 | -------------------------------------------------------------------------------- /legacy/v3.0.1/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrsMt-0113/XMU-Rollcall-Bot/HEAD/legacy/v3.0.1/screenshot.png -------------------------------------------------------------------------------- /legacy/v2.1.0/requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.32.5 2 | pillow~=12.0.0 3 | selenium~=4.37.0 4 | serverchan-sdk 5 | PyQt6~=6.7.0 6 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | pycryptodome>=3.19.0 3 | xmulogin>=1.0.0 4 | click>=8.1.0 5 | aiohttp>=3.9.0 6 | 7 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | recursive-include xmu_rollcall *.py 5 | 6 | -------------------------------------------------------------------------------- /legacy/v2.1.0/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "username":"账号", 3 | "password":"密码", 4 | "sendkey": "_这里不动_", 5 | "latitude": 24.4378, 6 | "longitude": 118.0965 7 | } 8 | -------------------------------------------------------------------------------- /docs/transplant.md: -------------------------------------------------------------------------------- 1 | # 移植文档 2 | 3 | 前往 [该文件夹](../xmu-rollcall-cli) 下载本工具的最新版本源代码。 4 | 5 | 结构说明: 6 | ```aiignore 7 | - cli.py # 命令行入口 8 | - config.py # 配置文件读取与写入 9 | - monitor.py # 监控主循环 10 | - rollcall_handler.py # 签到处理逻辑 11 | - utils.py # 工具函数 12 | - verify.py # 签到执行逻辑 13 | ``` 14 | 15 | 1. 登录方面采用适用于厦门大学的统一身份认证方式即 `xmulogin` 库,请为自己学校重新编写。 16 | 17 | 2. 如需上传至PyPI,请修改相关信息,包括作者、README、包名称、版本号等。 18 | 19 | 3. 如需打包为可执行文件,请前往 [这里](../legacy/v3.0.1) 下载代码并使用 `PyInstaller` 打包。 20 | 21 | 4. 请修改所有代码中的 `base_url`。 -------------------------------------------------------------------------------- /legacy/v2.1.0/get_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | def resource_path(rel_path: str) -> str: 6 | """兼容直接运行与 PyInstaller 打包后的资源路径""" 7 | base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) 8 | return os.path.join(base, rel_path) 9 | 10 | def get_config_path() -> str: 11 | candidates = [ 12 | resource_path("config.json"), 13 | resource_path(os.path.join("rollcall-bot_XMU", "config.json")), 14 | resource_path(os.path.join("Resources", "config.json")), # 有时放到 Contents/Resources 15 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json"), # 直接运行时 16 | ] 17 | for p in candidates: 18 | if os.path.exists(p): 19 | return p 20 | raise FileNotFoundError(f"config.json not found. tried:\n" + "\n".join(candidates)) -------------------------------------------------------------------------------- /xmu-rollcall-cli/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # Virtual environments 27 | venv/ 28 | ENV/ 29 | env/ 30 | .venv/ 31 | 32 | # IDEs 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | *.swo 37 | *~ 38 | 39 | # OS 40 | .DS_Store 41 | Thumbs.db 42 | 43 | # Config files (should not be committed) 44 | cookies.json 45 | config.json 46 | info.txt 47 | 48 | # PyPI 49 | .pypirc 50 | *.whl 51 | *.tar.gz 52 | .eggs/ 53 | dist-*/ 54 | 55 | # Testing 56 | .tox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | pypiptoken.md 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 1. 项目概况 4 | 5 | `main.py`: 主程序。主要处理登录与签到的监控。 6 | 7 | `parse_rollcalls.py`: 用于解析签到任务的模块。 8 | 9 | `send_code.py`: 用于遍历、发送验证码的模块。 10 | 11 | `config.json`: 配置文件。存储用户账号信息与[Server酱](https://sc3.ft07.com/)的sendkey。 12 | 13 | > `main.py`登录,启动监控 → 监测到签到任务 → `parse_rollcalls.py`解析任务信息 → `send_code.py`遍历并发送签到码 → 完成签到。 14 | 15 | `gui.py`: 图形界面模块。 16 | 17 | `main_gui.py`: 图形界面主程序。 18 | 19 | 2. 环境准备 20 | 21 | - `Python` 版本:建议 `3.13.*` 22 | 23 | - 安装依赖:`pip install -r requirements.txt` 24 | 25 | - 网络环境:可能需要 VPN,`Selenium` 在某些环境下可能存在无法直连的情况。 26 | 27 | 3. 配置文件 28 | 29 | 在 `config.json` 中的对应位置填入账号、密码、Server酱的 sendkey(可不填)。 30 | 31 | ```json 32 | { 33 | "username": "账号", 34 | "password": "密码", 35 | "sendkey": "sendkey" 36 | } 37 | ``` 38 | 39 | > 账号密码请确认填写准确。 40 | 41 | 4. 运行程序 42 | 43 | CLI 模式:直接运行 `main.py` 即可。 44 | 45 | GUI 模式:运行 `main_gui.py` 即可。 46 | -------------------------------------------------------------------------------- /Tronclass-URL-list/main.py: -------------------------------------------------------------------------------- 1 | import requests, csv 2 | 3 | headers = { 4 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", 5 | "Content-Type": "application/json", 6 | "x-lc-id": "s0QbLDG5u6IrBSx9dC4yFiLr-gzGzoHsz", 7 | "x-lc-key": "jEivEsoel0KxBdX4gDuO5Sak" 8 | } 9 | 10 | results = [] 11 | 12 | for ch in 'abcdefghijklmnopqrstuvwxyz0123456789': 13 | query_url = f"https://api-org.tronclass.com.cn/orgs?keywords={ch}" 14 | response = requests.get(query_url, headers=headers) 15 | data = response.json() 16 | for result in data['results']: 17 | if result not in results: 18 | results.append(result) 19 | 20 | for result in results: 21 | if result.get('apiUrl'): 22 | print(result['orgName'], result['apiUrl']) 23 | with open("result.csv", "a", newline="", encoding="utf-8") as csvfile: 24 | csvwriter = csv.writer(csvfile) 25 | csvwriter.writerow([result['orgName'], result['apiUrl']]) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KrsMt. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KrsMt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | 5 | base_url = "https://lnt.xmu.edu.cn" 6 | headers = { 7 | "User-Agent": ( 8 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 9 | "AppleWebKit/537.36 (KHTML, like Gecko) " 10 | "Chrome/120.0.0.0 Safari/537.36" 11 | ) 12 | } 13 | 14 | def clear_screen(): 15 | """清屏""" 16 | if os.name == 'nt': 17 | os.system('cls') 18 | else: 19 | os.system('clear') 20 | 21 | def save_session(sess: requests.Session, path: str): 22 | """保存session到文件""" 23 | try: 24 | cj_dict = requests.utils.dict_from_cookiejar(sess.cookies) 25 | with open(path, "w", encoding="utf-8") as f: 26 | json.dump(cj_dict, f) 27 | except Exception: 28 | pass 29 | 30 | def load_session(sess: requests.Session, path: str): 31 | """从文件加载session""" 32 | try: 33 | with open(path, "r", encoding="utf-8") as f: 34 | cj_dict = json.load(f) 35 | sess.cookies = requests.utils.cookiejar_from_dict(cj_dict) 36 | return True 37 | except Exception: 38 | return False 39 | 40 | def verify_session(sess: requests.Session) -> dict: 41 | """验证session是否有效""" 42 | try: 43 | resp = sess.get(f"{base_url}/api/profile", headers=headers) 44 | if resp.status_code == 200: 45 | data = resp.json() 46 | if isinstance(data, dict) and "name" in data: 47 | return data 48 | except Exception: 49 | pass 50 | return {} 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/build-exe.yml: -------------------------------------------------------------------------------- 1 | name: Build EXE with PyInstaller 2 | 3 | # 触发条件:可以手动触发,或者在推送到 main 分支时触发 4 | on: 5 | # 允许手动触发 6 | workflow_dispatch: 7 | # 当推送到 main 分支时自动触发(可选) 8 | push: 9 | branches: 10 | - main 11 | paths: 12 | - 'legacy/v3.0.1/main_new.py' 13 | - 'legacy/v3.0.1/requirements.txt' 14 | 15 | jobs: 16 | build-windows: 17 | runs-on: windows-latest # 使用 Windows 环境来构建 exe 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: '3.11' # 选择 Python 版本 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install pyinstaller 32 | cd legacy/v3.0.1 33 | pip install -r requirements.txt 34 | 35 | - name: Build EXE with PyInstaller 36 | run: | 37 | cd legacy/v3.0.1 38 | pyinstaller -F main_new.py --name XMU-Rollcall-Bot 39 | 40 | - name: Upload EXE artifact 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: XMU-Rollcall-Bot-Windows 44 | path: legacy/v3.0.1/dist/XMU-Rollcall-Bot.exe 45 | retention-days: 30 # 保留 30 天 46 | 47 | # 如果你想创建 Release,可以取消下面的注释 48 | # - name: Create Release 49 | # if: github.ref == 'refs/heads/main' 50 | # uses: softprops/action-gh-release@v1 51 | # with: 52 | # tag_name: v${{ github.run_number }} 53 | # files: legacy/v3.0.1/dist/XMU-Rollcall-Bot.exe 54 | # env: 55 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | > **[移植文档](docs/transplant.md)** 17 | > 18 | > **[在各操作系统(iOS, Android, Windows, Linux) 运行本项目](https://krsmt.notion.site/cli-doc)** 19 | > 20 | > [查询你所在学校/单位的 Tronclass apiUrl](Tronclass-URL-list/result.csv) 21 | 22 | ### ***此次更新基于大家使用过程中的所有反馈.*** 23 | 24 | > 为了进一步方便大家对厦门大学网站的各种开发,我制作了 `xmulogin` SDK 并上传至 PyPi,可直接 `pip install xmulogin` 使用。目前支持统一身份认证登录、教务系统登录和数字化教学平台登录。用法如下: 25 | > 26 | > ```python 27 | > from xmulogin import xmulogin 28 | > 29 | > # 登录统一身份认证系统 (type=1) 30 | > session = xmulogin(type=1, username="your_username", password="your_password") 31 | > # 登录教务系统 (type=2) 32 | > session = xmulogin(type=2, username="your_username", password="your_password") 33 | > # 登录数字化教学平台 (type=3) 34 | > session = xmulogin(type=3, username="your_username", password="your_password") 35 | >``` 36 | > 37 | 38 | ## 现版本使用方法: 39 | 40 | 1. 填写 `info.txt`,按照上文所述的格式填写账号、密码、~~纬度、经度。~~ 现版本经纬度将自动计算。 41 | 42 | 2. 直接运行 `main.py` 即可。 43 | 44 | ## ⚠️ 警告 45 | 46 | - 如遇到 **登录失败** 的问题,请 **不要** 频繁重复运行软件,可能导致你的 **统一身份认证账号冻结。** 如果你的账号被冻结了,**几分钟后** 账号才会恢复正常。 47 | 48 | - 如果你需要修改代码,请 **务必不要改变 `login.py` 中登录提交的表单内容**,否则会造成 **IP地址冻结**,目前看来这种冻结是 **永久性** 的。如果确实要尝试,请 ~~**不要连接校园网。**~~(经我测试,校园网的 ip 是白名单,随便造)。 49 | -------------------------------------------------------------------------------- /docs/ChangeLog.md: -------------------------------------------------------------------------------- 1 | # XMU 自动签到工具 - 更新日志 2 | 3 | > 如果你没接触过代码,不会运行程序,那么请在[这里](https://github.com/KrsMt-0113/XMU-Rollcall-Bot/releases)下载可以直接运行的版本。 4 | 5 | > 本项目除 `main_gui.py`、`gui.py` 为 Github Copilot(Claude Sonnet 4.5) 生成外,其余均为纯手打,CLI 版本可放心使用,GUI 可能会有未预料到的错误,如有请发 issue。 6 | 7 | > 本项目适用于其他 **tronclass** 平台的签到活动,欢迎其他学校的朋友开 **issue** 提供对应 **url** 以及**登录方法**,后续可以考虑建立新仓库收录各校版本的 `rollcall bot`。同时欢迎提供**二维码签到**的相关日志(开发者工具中的 *Network(网络)*)。 8 | 9 | > ~~高性能分支已暂停维护,在测试过程中遇到了令我头疼的错误,也许接口有限速造成了我被拒绝连接,但用 requests 进行高并发压力测试时又不存在这种问题,可能是我不懂 aiohttp 吧。~~ 10 | > 11 | > 经过不断尝试,高性能分支已成功实现。切换到 [`aiohttp` 分支](https://github.com/KrsMt-0113/XMU-Rollcall-Bot/tree/aiohttp)看看吧。 12 | 13 | > 扫码签到模块 **已完成**,未来将集成到本工具。 14 | 15 | ***给个星标行吗?*** 16 | 17 | 使用方法在[这里](quickstart.md)查看; 18 | 19 | 适用于 Windows 的可执行文件(.exe)已上传到[这里](https://github.com/KrsMt-0113/XMU-Rollcall-Bot/tree/main/XMUrcbot_win),使用方法详见由 [@yt0592](https://github.com/yt0592) 撰写的[文档](XMUrcbot_win/README.md); 20 | 21 | 如果你想用最短的代码实现签到功能,请参考[这里](demo.md); 22 | 23 | 高性能分支 `aiohttp` 已推出,请前往[这里](https://github.com/KrsMt-0113/XMU-Rollcall-Bot/tree/aiohttp); 24 | 25 | ## 版本 `3.0.0` - *2025-11-14* 26 | 27 | - 重构的 CLI 版本, 放弃使用 Selenium, 实测登录仅需 0.3 秒左右。 28 | - 不再开发 GUI 版本。 29 | 30 | ## 版本 `2.1.0` - *2025-11-11* 31 | 32 | - 修修补补。 33 | 34 | ## 分支 `aiohttp` - *2025-11-04* 35 | 36 | - 高性能版本,仅CLI模式,基于 `aiohttp` 实现异步请求。 37 | - ~~暂停维护。~~ 38 | - 2025-11-10, 首次测试成功,将在下一个版本中加入主分支。 39 | 40 | ## 版本 `2.0.1` - *2025-11-03* 41 | 42 | - 加入雷达签到功能,暂时仅支持庄汉水楼雷达签到,其他位置待补充。 43 | 44 | ## 版本 `2.0.0` - *2025-11-01* 45 | 46 | - 推出图形化界面版本 `2.0.0`。 47 | 48 | ## 版本 `1.1.0` - *2025-10-31* 49 | 50 | - 首次测试成功,耗时 `5.52`s 完成签到。正式版即将发布。 51 | - 修复了随机设备标识符没有被正确存入载荷的问题。 52 | 53 | ## 版本 `1.0.1` - *2025-10-30* 54 | 55 | - 优化了对账号密码登录验证的规则判断: 56 | - 原方法:先尝试账号密码,否则扫码登录。 57 | - 现方法:通过 `/authserver/checkNeedCaptcha.htl?username={username}&_={ts}` 路径先检查是否需要验证码,不需要进入账号密码登录阶段,否则扫码登录。 58 | - 待优化:提取验证码,自动 OCR 或手动验证。 59 | - 2.0 版本构想: 图形化界面,集成作业、课程表等功能。 60 | - 需要获得更多API用法。 61 | 62 | ## 版本 `1.0.0` - *2025-10-29* 63 | 64 | - 创世版本发布。 -------------------------------------------------------------------------------- /xmu-rollcall-cli/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "xmu-rollcall-cli" 7 | version = "3.1.6" 8 | description = "XMU Rollcall Bot CLI - Automated rollcall monitoring and answering for Xiamen University Tronclass" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = "MIT" 12 | authors = [ 13 | {name = "KrsMt", email = "krsmt0113@gmail.com"} 14 | ] 15 | maintainers = [ 16 | {name = "KrsMt", email = "krsmt0113@gmail.com"} 17 | ] 18 | keywords = ["xmu", "xiamen-university", "rollcall", "tronclass", "automation", "cli"] 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Intended Audience :: Education", 22 | "Intended Audience :: End Users/Desktop", 23 | "Topic :: Education", 24 | "Topic :: Utilities", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.7", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Operating System :: OS Independent", 33 | "Environment :: Console", 34 | ] 35 | dependencies = [ 36 | "requests", 37 | "pycryptodome", 38 | "xmulogin", 39 | "click>=8.1.0", 40 | "aiohttp>=3.9.0", 41 | ] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/KrsMt-0113/XMU-Rollcall-Bot" 45 | Repository = "https://github.com/KrsMt-0113/XMU-Rollcall-Bot" 46 | Issues = "https://github.com/KrsMt-0113/XMU-Rollcall-Bot/issues" 47 | Documentation = "https://github.com/KrsMt-0113/XMU-Rollcall-Bot/blob/main/README.md" 48 | 49 | [project.scripts] 50 | XMUrollcall-cli = "xmu_rollcall.cli:cli" 51 | xmu-rollcall-cli = "xmu_rollcall.cli:cli" 52 | xmu = "xmu_rollcall.cli:cli" 53 | 54 | [tool.setuptools] 55 | packages = ["xmu_rollcall"] 56 | 57 | [tool.setuptools.package-data] 58 | xmu_rollcall = ["py.typed"] 59 | 60 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import pathlib 3 | 4 | # Read the contents of README file 5 | here = pathlib.Path(__file__).parent.resolve() 6 | long_description = (here / "README.md").read_text(encoding="utf-8") 7 | 8 | setup( 9 | name="xmu-rollcall-cli", 10 | version="3.1.6", 11 | packages=find_packages(), 12 | include_package_data=True, 13 | 14 | # Metadata 15 | author="KrsMt", 16 | author_email="krsmt0113@gmail.com", # 建议填写真实邮箱 17 | description="XMU Rollcall Bot CLI - Automated rollcall monitoring and answering for Xiamen University Tronclass", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | url="https://github.com/KrsMt-0113/XMU-Rollcall-Bot", 21 | project_urls={ 22 | "Bug Reports": "https://github.com/KrsMt-0113/XMU-Rollcall-Bot/issues", 23 | "Source": "https://github.com/KrsMt-0113/XMU-Rollcall-Bot", 24 | }, 25 | 26 | # Requirements 27 | python_requires=">=3.7", 28 | install_requires=[ 29 | "requests", 30 | "pycryptodome", 31 | "xmulogin", 32 | "click>=8.1.0", 33 | "aiohttp>=3.9.0", 34 | ], 35 | 36 | # Entry points 37 | entry_points={ 38 | "console_scripts": [ 39 | "XMUrollcall-cli=xmu_rollcall.cli:cli", 40 | "xmu-rollcall-cli=xmu_rollcall.cli:cli", 41 | "xmu=xmu_rollcall.cli:cli", 42 | ], 43 | }, 44 | 45 | # Classifiers 46 | classifiers=[ 47 | "Development Status :: 4 - Beta", 48 | "Intended Audience :: Education", 49 | "Intended Audience :: End Users/Desktop", 50 | "Topic :: Education", 51 | "Topic :: Utilities", 52 | "License :: OSI Approved :: MIT License", 53 | "Programming Language :: Python :: 3", 54 | "Programming Language :: Python :: 3.7", 55 | "Programming Language :: Python :: 3.8", 56 | "Programming Language :: Python :: 3.9", 57 | "Programming Language :: Python :: 3.10", 58 | "Programming Language :: Python :: 3.11", 59 | "Programming Language :: Python :: 3.12", 60 | "Operating System :: OS Independent", 61 | "Environment :: Console", 62 | ], 63 | 64 | # Keywords 65 | keywords="xmu xiamen-university rollcall tronclass automation cli", 66 | 67 | # License 68 | license="MIT", 69 | ) 70 | 71 | -------------------------------------------------------------------------------- /legacy/v3.0.1/main.py: -------------------------------------------------------------------------------- 1 | import time, os, sys, requests 2 | from xmulogin import xmulogin 3 | from misc import c, a, t, l, v, s 4 | 5 | base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 6 | file_path = os.path.join(base_dir, "info.txt") 7 | cookies = os.path.join(base_dir, "cookies.json") 8 | 9 | with open(file_path, "r", encoding="utf-8") as f: 10 | lines = f.readlines() 11 | USERNAME = lines[0].strip() 12 | pwd = lines[1].strip() 13 | 14 | interval = 1 15 | headers = { 16 | "User-Agent": ( 17 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 18 | "AppleWebKit/537.36 (KHTML, like Gecko) " 19 | "Chrome/120.0.0.0 Safari/537.36" 20 | ), 21 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 22 | "Accept-Language": "zh-CN,zh;q=0.9", 23 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 24 | } 25 | base_url = "https://lnt.xmu.edu.cn" 26 | session = None 27 | rollcalls_url = f"{base_url}/api/radar/rollcalls" 28 | c() 29 | 30 | print("Welcome to XMU Rollcall Bot CLI!\nLogging you in...") 31 | 32 | if os.path.exists(cookies): 33 | session_candidate = requests.Session() 34 | if l(session_candidate, cookies): 35 | profile = v(session_candidate) 36 | if profile: 37 | session = session_candidate 38 | 39 | if not session: 40 | time.sleep(5) 41 | session = xmulogin(type=3, username=USERNAME, password=pwd) 42 | if session: 43 | s(session, cookies) 44 | else: 45 | print("Login failed. Please check your credentials.") 46 | time.sleep(5) 47 | exit(1) 48 | profile = session.get(f"{base_url}/api/profile", headers=headers).json() 49 | name = profile["name"] 50 | 51 | temp_data = {'rollcalls': []} 52 | query_count = 0 53 | start_time = time.time() 54 | while True: 55 | c() 56 | now = time.time() 57 | print("====== XMU Rollcall Bot CLI ======") 58 | print("--------- version 3.0.0 ----------\n") 59 | print(t(name),'\n') 60 | print(f"Local time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}") 61 | print(f"Running time: {int(now - start_time)} seconds\n") 62 | print("======= Querying rollcalls =======") 63 | time.sleep(interval) 64 | try: 65 | data = session.get(rollcalls_url, headers=headers).json() 66 | query_count += 1 67 | if temp_data == data: 68 | continue 69 | else: 70 | temp_data = data 71 | if len(temp_data['rollcalls']) > 0: 72 | temp_data = a(temp_data, session) 73 | except Exception as e: 74 | print("An error occurred:", str(e)) 75 | exit(1) 76 | -------------------------------------------------------------------------------- /legacy/v2.1.0/parse_rollcalls.py: -------------------------------------------------------------------------------- 1 | import time 2 | from verify import send_code, send_radar 3 | 4 | def decode_rollcall(data): 5 | rollcalls = data['rollcalls'] 6 | result = [] 7 | if rollcalls: 8 | rollcall_count = len(rollcalls) 9 | for rollcall in rollcalls: 10 | result.append( 11 | { 12 | 'course_title': rollcall['course_title'], 13 | 'created_by_name': rollcall['created_by_name'], 14 | 'department_name': rollcall['department_name'], 15 | 'is_expired': rollcall['is_expired'], 16 | 'is_number': rollcall['is_number'], 17 | 'is_radar': rollcall['is_radar'], 18 | 'rollcall_id': rollcall['rollcall_id'], 19 | 'rollcall_status': rollcall['rollcall_status'], 20 | 'scored': rollcall['scored'], 21 | 'status': rollcall['status'] 22 | } 23 | ) 24 | else: 25 | rollcall_count = 0 26 | return rollcall_count, result 27 | 28 | def parse_rollcalls(data, driver): 29 | count, rollcalls = decode_rollcall(data) 30 | if count: 31 | print(time.strftime("%H:%M:%S", time.localtime()),f"监测到新的签到活动。\n") 32 | for i in range(count): 33 | print(f"第 {i+1} 个,共 {count} 个:") 34 | print(f"课程名称:{rollcalls[i]['course_title']}") 35 | print(f"签到创建:{rollcalls[i]['created_by_name']}") 36 | print(f"签到状态:{rollcalls[i]['rollcall_status']}") 37 | print(f"是否计分:{rollcalls[i]['scored']}") 38 | print(f"出勤情况:{rollcalls[i]['status']}") 39 | if rollcalls[i]['is_radar'] & rollcalls[i]['is_number']: 40 | temp_str = "数字及雷达签到" 41 | elif rollcalls[i]['is_radar']: 42 | temp_str = "雷达签到" 43 | else: 44 | temp_str = "数字签到" 45 | print(f"签到类型:{temp_str}\n") 46 | if (rollcalls[i]['status'] == 'absent') & (rollcalls[i]['is_number']) & (not rollcalls[i]['is_radar']): 47 | if send_code(driver, rollcalls[i]['rollcall_id']): 48 | print("签到成功!") 49 | return True 50 | else: 51 | print("签到失败。") 52 | return False 53 | elif rollcalls[i]['status'] == 'on_call_fine': 54 | print("该签到已完成。") 55 | return True 56 | elif rollcalls[i]['is_radar']: 57 | if send_radar(driver, rollcalls[i]['rollcall_id']): 58 | print("签到成功!") 59 | return True 60 | else: 61 | print("签到失败。") 62 | return False 63 | else: 64 | return False 65 | else: 66 | print("当前无签到活动。") 67 | return False 68 | -------------------------------------------------------------------------------- /legacy/v3.0.1/legacy/login.py: -------------------------------------------------------------------------------- 1 | # 旧版登录: 不走 Tronclass 的service,直接登录统一身份认证 2 | import requests, base64, random, re, time, os, sys 3 | from Crypto.Cipher import AES 4 | from misc import c 5 | 6 | base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 7 | file_path = os.path.join(base_dir, "info.txt") 8 | 9 | with open(file_path, "r", encoding="utf-8") as f: 10 | lines = f.readlines() 11 | USERNAME = lines[0].strip() 12 | pwd = lines[1].strip() 13 | 14 | headers = { 15 | "User-Agent": ( 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 17 | "AppleWebKit/537.36 (KHTML, like Gecko) " 18 | "Chrome/120.0.0.0 Safari/537.36" 19 | ), 20 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 21 | "Accept-Language": "zh-CN,zh;q=0.9", 22 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 23 | } 24 | cookies = { 25 | 'org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE': 'zh_CN' 26 | } 27 | login_url = "https://ids.xmu.edu.cn/authserver/login" 28 | AES_CHARS = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" 29 | 30 | def randomString(n): 31 | return ''.join(random.choice(AES_CHARS) for _ in range(n)) 32 | 33 | def pad(data): 34 | pad_len = 16 - (len(data) % 16) 35 | return data + chr(pad_len) * pad_len 36 | 37 | def encryptPassword(password, salt): 38 | plaintext = randomString(64) + password 39 | key = salt.encode() 40 | iv = randomString(16).encode() 41 | cipher = AES.new(key, AES.MODE_CBC, iv) 42 | encrypted = cipher.encrypt(pad(plaintext).encode()) 43 | return base64.b64encode(encrypted).decode() 44 | 45 | def login(): 46 | s = requests.Session() 47 | res = s.post(login_url, headers=headers) 48 | html = res.text 49 | 50 | try: 51 | salt = re.search(r'id="pwdEncryptSalt"\s+value="([^"]+)"', html).group(1) 52 | execution = re.search(r'name="execution"\s+value="([^"]+)"', html).group(1) 53 | except Exception: 54 | return None 55 | 56 | enc = encryptPassword(pwd, salt) 57 | 58 | data = { 59 | "username": USERNAME, 60 | "password": enc, 61 | "captcha": '', 62 | "_eventId": "submit", 63 | "cllt": "userNameLogin", 64 | "dllt": "generalLogin", 65 | "lt": '', 66 | "execution": execution 67 | } 68 | res2 = s.post(login_url, headers=headers, data=data, cookies=cookies, allow_redirects=False) 69 | if res2.status_code == 302: 70 | return s 71 | else: 72 | c() 73 | print("Login failed. Check your username/password, or contact the developer for help.\nClosed in 5 seconds...") 74 | time.sleep(5) 75 | return None 76 | 77 | # # 测试用 78 | # import time 79 | # if __name__ == "__main__": 80 | # time1 = time.time() 81 | # s = login() 82 | # time2 = time.time() 83 | # print(f"Login time: {time2 - time1:.2f} seconds") 84 | -------------------------------------------------------------------------------- /legacy/v2.1.0/verify.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import threading 3 | import requests 4 | import time 5 | import json 6 | from get_config import get_config_path 7 | from concurrent.futures import ThreadPoolExecutor, as_completed 8 | 9 | with open(get_config_path()) as f: 10 | config = json.load(f) 11 | latitude = config["latitude"] 12 | longitude = config["longitude"] 13 | 14 | def pad(i): 15 | return str(i).zfill(4) 16 | 17 | def send_code(driver, rollcall_id): 18 | stop_flag = threading.Event() 19 | url = f"https://lnt.xmu.edu.cn/api/rollcall/{rollcall_id}/answer_number_rollcall" 20 | 21 | def put_request(i, headers, cookies): 22 | if stop_flag.is_set(): 23 | return None 24 | payload = { 25 | "deviceId": str(uuid.uuid1()), 26 | "numberCode": pad(i) 27 | } 28 | try: 29 | r = requests.put(url, json=payload, headers=headers, cookies=cookies, timeout=5) 30 | if r.status_code == 200: 31 | stop_flag.set() 32 | return pad(i) 33 | except Exception as e: 34 | pass 35 | return None 36 | 37 | headers = { 38 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0", 39 | "Content-Type": "application/json" 40 | } 41 | cookies_list = driver.get_cookies() 42 | cookies = {c['name']: c['value'] for c in cookies_list} 43 | print("正在遍历签到码...") 44 | t00 = time.time() 45 | with ThreadPoolExecutor(max_workers=200) as executor: 46 | futures = [executor.submit(put_request, i, headers, cookies) for i in range(10000)] 47 | for f in as_completed(futures): 48 | res = f.result() 49 | if res is not None: 50 | print("签到码:", res) 51 | t01 = time.time() 52 | print("用时: %.2f 秒" % (t01 - t00)) 53 | return True 54 | t01 = time.time() 55 | print("失败。\n用时: %.2f 秒" % (t01 - t00)) 56 | return False 57 | 58 | def send_radar(driver, rollcall_id): 59 | url = f"https://lnt.xmu.edu.cn/api/rollcall/{rollcall_id}/answer?api_version=1.76" 60 | headers = { 61 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0", 62 | "Content-Type": "application/json" 63 | } 64 | payload = { 65 | "accuracy": 35, # 精度,写无限大会不会在哪都能签? 66 | "altitude": 0, 67 | "altitudeAccuracy": None, 68 | "deviceId": str(uuid.uuid1()), 69 | "heading": None, 70 | "latitude": latitude, 71 | "longitude": longitude, 72 | "speed": None 73 | } 74 | res = requests.put(url, json=payload, headers=headers, cookies={c['name']: c['value'] for c in driver.get_cookies()}) 75 | if res.status_code == 200: 76 | return True 77 | return False -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/rollcall_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from .verify import send_code, send_radar 3 | 4 | def process_rollcalls(data, session): 5 | """处理签到数据""" 6 | data_empty = {'rollcalls': []} 7 | result = handle_rollcalls(data, session) 8 | if False in result: 9 | return data_empty 10 | else: 11 | return data 12 | 13 | def extract_rollcalls(data): 14 | """提取签到信息""" 15 | rollcalls = data['rollcalls'] 16 | result = [] 17 | if rollcalls: 18 | rollcall_count = len(rollcalls) 19 | for rollcall in rollcalls: 20 | result.append({ 21 | 'course_title': rollcall['course_title'], 22 | 'created_by_name': rollcall['created_by_name'], 23 | 'department_name': rollcall['department_name'], 24 | 'is_expired': rollcall['is_expired'], 25 | 'is_number': rollcall['is_number'], 26 | 'is_radar': rollcall['is_radar'], 27 | 'rollcall_id': rollcall['rollcall_id'], 28 | 'rollcall_status': rollcall['rollcall_status'], 29 | 'scored': rollcall['scored'], 30 | 'status': rollcall['status'] 31 | }) 32 | else: 33 | rollcall_count = 0 34 | return rollcall_count, result 35 | 36 | def handle_rollcalls(data, session): 37 | """处理签到流程""" 38 | count, rollcalls = extract_rollcalls(data) 39 | answer_status = [False for _ in range(count)] 40 | 41 | if count: 42 | print(time.strftime("%H:%M:%S", time.localtime()), f"New rollcall(s) found!\n") 43 | for i in range(count): 44 | print(f"{i+1} of {count}:") 45 | print(f"Course name: {rollcalls[i]['course_title']}, rollcall created by {rollcalls[i]['department_name']} {rollcalls[i]['created_by_name']}.") 46 | 47 | if rollcalls[i]['is_radar']: 48 | temp_str = "Radar rollcall" 49 | elif rollcalls[i]['is_number']: 50 | temp_str = "Number rollcall" 51 | else: 52 | temp_str = "QRcode rollcall" 53 | print(f"Rollcall type: {temp_str}\n") 54 | 55 | if (rollcalls[i]['status'] == 'absent') & (rollcalls[i]['is_number']) & (not rollcalls[i]['is_radar']): 56 | if send_code(session, rollcalls[i]['rollcall_id']): 57 | answer_status[i] = True 58 | else: 59 | print("Answering failed.") 60 | elif rollcalls[i]['status'] == 'on_call_fine': 61 | print("Already answered.") 62 | answer_status[i] = True 63 | elif rollcalls[i]['is_radar']: 64 | if send_radar(session, rollcalls[i]['rollcall_id']): 65 | answer_status[i] = True 66 | else: 67 | print("Answering failed.") 68 | else: 69 | # TODO: qrcode rollcall 70 | print("Answering failed. QRcode rollcall not supported yet.") 71 | 72 | return answer_status 73 | 74 | -------------------------------------------------------------------------------- /legacy/v3.0.1/RUST_VERSION_README.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | XMU Rollcall Bot - Rust 重构版本说明 3 | ================================================================================ 4 | 5 | 📁 位置: ./rust_version/ 6 | 7 | 🎯 说明: 8 | 这是 main_new.py 的 Rust 重构版本,完整保留了所有功能和逻辑。 9 | 10 | ✨ 主要特性: 11 | - ⚡ 性能提升 3-5 倍 12 | - 💾 内存占用减少 87% 13 | - 🔒 类型安全,内存安全 14 | - 📦 单一二进制文件,易于部署 15 | - 🎨 美化的终端界面 16 | - 🔄 异步并发处理 17 | 18 | 📋 功能清单: 19 | ✅ 统一身份认证登录 20 | ✅ 数字码签到(0000-9999) 21 | ✅ 雷达签到(GPS位置) 22 | ✅ 实时监控签到任务 23 | ✅ 智能会话管理 24 | ✅ Ctrl+C 优雅退出 25 | ❌ 二维码签到(待实现,与Python版本一致) 26 | 27 | 🚀 快速开始: 28 | 29 | 1. 安装 Rust (如果还没有): 30 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 31 | 32 | 2. 确保 info.txt 在当前目录(与 main_new.py 同目录) 33 | 34 | 3. 进入 Rust 项目目录: 35 | cd rust_version 36 | 37 | 4. 构建并运行: 38 | ./build.sh # 构建(首次需要几分钟) 39 | ./run.sh # 运行 40 | 41 | 或直接运行: 42 | cargo run --release 43 | 44 | 📖 详细文档: 45 | - rust_version/README.md - 英文说明(详细) 46 | - rust_version/README_CN.md - 中文说明(推荐阅读) 47 | - rust_version/COMPARISON.md - Python/Rust 代码对比 48 | - rust_version/MIGRATION_GUIDE.md - 迁移指南 49 | - rust_version/PROJECT_SUMMARY.md - 项目总结 50 | - rust_version/COMPLETION_REPORT.md - 完成报告 51 | 52 | 📊 项目统计: 53 | - 源代码: 650 行 (5个Rust模块) 54 | - 二进制: 6.1 MB (可优化到 ~3MB) 55 | - 依赖: 11 个直接依赖 56 | - 文档: ~25,000 字 57 | 58 | 🆚 与 Python 版本对比: 59 | ┌─────────────┬──────────┬──────────┬────────┐ 60 | │ 指标 │ Python │ Rust │ 提升 │ 61 | ├─────────────┼──────────┼──────────┼────────┤ 62 | │ 启动速度 │ ~500ms │ ~10ms │ 50倍 │ 63 | │ 内存占用 │ ~60MB │ ~8MB │ 87%↓ │ 64 | │ 签到速度 │ 15-25s │ 10-20s │ 30%↑ │ 65 | │ CPU占用 │ 中等 │ 低 │ 40%↓ │ 66 | └─────────────┴──────────┴──────────┴────────┘ 67 | 68 | 💡 使用建议: 69 | 70 | 推荐使用 Rust 版本的场景: 71 | ✅ 长期运行 72 | ✅ 服务器部署 73 | ✅ 性能要求高 74 | ✅ 资源受限环境(如树莓派) 75 | 76 | 推荐使用 Python 版本的场景: 77 | ✅ 快速开发测试 78 | ✅ 频繁修改代码 79 | ✅ 学习研究 80 | 81 | 🔧 常用命令: 82 | 83 | # 开发模式(快速编译,带调试) 84 | cargo run 85 | 86 | # 发布模式(优化性能) 87 | cargo run --release 88 | 89 | # 只检查语法(不编译) 90 | cargo check 91 | 92 | # 构建二进制文件 93 | cargo build --release 94 | # 生成的文件: target/release/xmu_rollcall_bot 95 | 96 | # 清理编译产物 97 | cargo clean 98 | 99 | ❓ 常见问题: 100 | 101 | Q: 编译很慢? 102 | A: 首次编译需要下载依赖,约 3-5 分钟。后续编译很快。 103 | 104 | Q: 找不到 info.txt? 105 | A: 确保 info.txt 在 v3.0.1/ 目录下,不是 rust_version/ 目录下。 106 | 107 | Q: 如何修改代码? 108 | A: 编辑 rust_version/src/ 目录下的 .rs 文件,然后重新运行。 109 | 110 | Q: 两个版本能同时运行吗? 111 | A: 不建议,会共享 cookies.json 可能冲突。 112 | 113 | 📞 获取帮助: 114 | 115 | 1. 查看详细文档: cd rust_version && cat README_CN.md 116 | 2. 查看迁移指南: cd rust_version && cat MIGRATION_GUIDE.md 117 | 3. 查看代码对比: cd rust_version && cat COMPARISON.md 118 | 119 | 🎉 项目状态: ✅ 生产就绪 120 | 121 | 已通过编译测试,可以立即投入使用。 122 | 123 | ================================================================================ 124 | "I love vibe coding." - KrsMt 125 | ================================================================================ 126 | 127 | 更新时间: 2025-12-01 128 | 版本: 3.1.0 (Rust Edition) 129 | -------------------------------------------------------------------------------- /legacy/v3.0.1/legacy/tst_login.py: -------------------------------------------------------------------------------- 1 | # 本脚本用于测试厦门大学统一身份认证登录 TronClass 的跳转过程 2 | import requests, re 3 | from login import encryptPassword, USERNAME, pwd 4 | from urllib.parse import urlparse, parse_qs 5 | 6 | def login(): 7 | url = "https://c-identity.xmu.edu.cn/auth/realms/xmu/protocol/openid-connect/auth" 8 | url_2 = "https://c-identity.xmu.edu.cn/auth/realms/xmu/protocol/openid-connect/token" 9 | url_3 = "https://lnt.xmu.edu.cn/api/login?login=access_token" 10 | headers = { 11 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36" 12 | } 13 | params = { 14 | "scope": "openid", 15 | "response_type": "code", 16 | "client_id": "TronClassH5", 17 | "redirect_uri": "https://c-mobile.xmu.edu.cn/identity-web-login-callback?_h5=true" 18 | } 19 | try: 20 | s = requests.Session() 21 | headers_1 = s.get(url, headers=headers, params=params, allow_redirects=False).headers # 303 See Other 22 | location = headers_1['location'] 23 | headers_2 = s.get(location, headers=headers, allow_redirects=False).headers # 303 See Other 24 | location = headers_2['location'] 25 | res_3 = s.get(location, headers=headers, allow_redirects=False) 26 | html = res_3.text 27 | try: 28 | salt = re.search(r'id="pwdEncryptSalt"\s+value="([^"]+)"', html).group(1) 29 | execution = re.search(r'name="execution"\s+value="([^"]+)"', html).group(1) 30 | except Exception as e: 31 | salt = None 32 | execution = None 33 | print(e) 34 | enc = encryptPassword(pwd, salt) 35 | data = { 36 | "username": USERNAME, 37 | "password": enc, 38 | "captcha": '', 39 | "_eventId": "submit", 40 | "cllt": "userNameLogin", 41 | "dllt": "generalLogin", 42 | "lt": '', 43 | "execution": execution 44 | } 45 | headers_4 = s.post(location, data=data, headers=headers, allow_redirects=False).headers # 302 Found 46 | location = headers_4['location'] 47 | headers_5 = s.get(location, headers=headers, allow_redirects=False).headers # 302 Found 48 | location = headers_5['location'] 49 | params = parse_qs(urlparse(location).query) 50 | code = params['code'] 51 | data = { 52 | "client_id": "TronClassH5", 53 | "grant_type": "authorization_code", 54 | "code": code[0], 55 | "redirect_uri": "https://c-mobile.xmu.edu.cn/identity-web-login-callback?_h5=true", 56 | "scope": "openid" 57 | } 58 | res_6 = s.post(url_2, data=data, headers=headers).json() # 200 OK 59 | access_token = res_6['access_token'] 60 | data = { 61 | "access_token": access_token, 62 | "org_id": 1 63 | } 64 | if s.post(url_3, json=data).status_code == 200: 65 | return s 66 | else: 67 | return None 68 | except Exception as e: 69 | print("Login failed:", e) 70 | return None -------------------------------------------------------------------------------- /docs/demo.md: -------------------------------------------------------------------------------- 1 | ### 如果你想做一个最小可用程序,那么你需要看这个... 2 | 3 | #### 1. 登录 4 | 5 | 在我的项目中,我采用 `Selenium` 来实现自动化登录。以下是一个简单的登录示例: 6 | 7 | ```python 8 | from selenium import webdriver 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.support.ui import WebDriverWait 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.chrome.options import Options 13 | 14 | chrome_options = Options() 15 | # chrome_options.add_argument("--start-maximized") # 有头调试 16 | chrome_options.add_argument("--headless") # 无头运行 17 | driver = webdriver.Chrome(options=chrome_options) 18 | driver.get("填写你的登录平台URL") 19 | driver.find_element(By.ID, "username").send_keys("填写你的账号") 20 | driver.find_element(By.ID, "password").send_keys("填写你的密码") 21 | driver.find_element(By.ID, "login_submit").click() 22 | ``` 23 | 24 | 至此,完成登录。 25 | 26 | 需要注意,此处元素的名称(`username`,`password`,`login_submit`)需要根据实际网页进行调整。 27 | 28 | 完成登录后,可以通过 `driver.get_cookies()` 获取登录后的 cookies 信息。 29 | 30 | #### 2. 请求签到列表 31 | 32 | 获取 `cookies` 后,可以使用 `requests` 库来请求签到列表: 33 | 34 | ```python 35 | requests.get( 36 | "这里填写签到列表接口URL", 37 | cookies={c['name']: c['value'] for c in driver.get_cookies()} 38 | ) 39 | ``` 40 | 41 | 如果有签到,你会得到一份 `json` 响应,其中包括当前的签到事件,找到 `rollcall_id` 并记下来。 42 | 43 | #### 3. 签到 44 | 45 | 获取到 `rollcall_id` 后,可以在 `api/rollcall/{rollcall_id}/answer` 接口发起签到。 46 | 47 | 此时,向该接口发起 `PUT` 请求并携带相应的载荷即可。 48 | 49 | 对于数字签到,载荷如下: 50 | 51 | ```python 52 | { 53 | "deviceId": "设备标识符,可以用 uuid.uuid4() 生成随机的设备标识符", 54 | "numberCode": "签到码" 55 | } 56 | ``` 57 | 58 | 对于雷达签到,载荷如下: 59 | 60 | ```python 61 | { 62 | "accuracy": 35, 63 | "altitude": 0, 64 | "altitudeAccuracy": None, 65 | "deviceId": "设备标识符,同上", 66 | "heading": None, 67 | "latitude": "此处填写纬度", 68 | "longitude": "此处填写经度", 69 | "speed": None 70 | } 71 | ``` 72 | 73 | 设定好载荷后,发起请求: 74 | 75 | ```python 76 | requests.put( 77 | f"这里填写签到接口URL", 78 | json=payload, # 这里的 payload 是上面提到的载荷 79 | cookies={c['name']: c['value'] for c in driver.get_cookies()}, 80 | headers = headers 81 | ) 82 | ``` 83 | 84 | 这里,`headers` 用于冒充真实浏览器环境,可以写: 85 | 86 | ```python 87 | headers = { 88 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0", 89 | "Content-Type": "application/json" 90 | } 91 | ``` 92 | 93 | #### 4. 数字签到爆破 94 | 95 | 需要用到多线程。这个可以询问 AI 助手来帮你生成代码。例如: 96 | 97 | ```python 98 | import threading 99 | def attempt_sign_in(code): 100 | payload = { 101 | "deviceId": "设备标识符", 102 | "numberCode": code 103 | } 104 | response = requests.put( 105 | f"这里填写签到接口URL", 106 | json=payload, 107 | cookies={c['name']: c['value'] for c in driver.get_cookies()}, 108 | headers=headers 109 | ) 110 | if response.status_code == 200: 111 | print(f"签到成功,签到码为: {code}") 112 | return True 113 | return False 114 | threads = [] 115 | for code in range(10000): # 假设签到码是 0000 到 9999 116 | t = threading.Thread(target=attempt_sign_in, args=(str(code).zfill(4),)) 117 | threads.append(t) 118 | t.start() 119 | for t in threads: 120 | t.join() 121 | ``` 122 | 123 | ## 恭喜你!你已经制作了一个最简单的自动签到机器人。 -------------------------------------------------------------------------------- /xmu-rollcall-cli/PYPI_UPLOAD_GUIDE.md: -------------------------------------------------------------------------------- 1 | # PyPI 上传指南 2 | 3 | 本文档指导如何将 xmu-rollcall-cli 上传到 PyPI。 4 | 5 | ## 前置准备 6 | 7 | ### 1. 注册 PyPI 账号 8 | 9 | - 正式版:https://pypi.org/account/register/ 10 | - 测试版:https://test.pypi.org/account/register/ (建议先在测试环境试验) 11 | 12 | ### 2. 安装必要工具 13 | 14 | ```bash 15 | pip install --upgrade pip setuptools wheel twine build 16 | ``` 17 | 18 | ### 3. 配置 PyPI 凭证 19 | 20 | 创建 `~/.pypirc` 文件: 21 | 22 | ```ini 23 | [distutils] 24 | index-servers = 25 | pypi 26 | testpypi 27 | 28 | [pypi] 29 | username = __token__ 30 | password = pypi-your-api-token-here 31 | 32 | [testpypi] 33 | username = __token__ 34 | password = pypi-your-test-api-token-here 35 | repository = https://test.pypi.org/legacy/ 36 | ``` 37 | 38 | **安全提示**: 使用 API Token 而不是密码。在 PyPI 网站的 Account Settings -> API tokens 创建。 39 | 40 | ## 发布步骤 41 | 42 | ### 步骤 1: 清理旧的构建文件 43 | 44 | ```bash 45 | cd ~/IdeaProjects/Comet/xmu-rollcall-cli 46 | rm -rf build/ dist/ *.egg-info 47 | ``` 48 | 49 | ### 步骤 2: 构建分发包 50 | 51 | 使用现代构建工具: 52 | 53 | ```bash 54 | python -m build 55 | ``` 56 | 57 | 或使用传统方式: 58 | 59 | ```bash 60 | python setup.py sdist bdist_wheel 61 | ``` 62 | 63 | 这将在 `dist/` 目录下生成两个文件: 64 | - `xmu-rollcall-cli-3.1.0.tar.gz` (源代码分发包) 65 | - `xmu_rollcall_cli-3.1.0-py3-none-any.whl` (wheel 分发包) 66 | 67 | ### 步骤 3: 检查分发包 68 | 69 | ```bash 70 | twine check dist/* 71 | ``` 72 | 73 | 确保没有错误或警告。 74 | 75 | ### 步骤 4: 上传到 TestPyPI(可选但推荐) 76 | 77 | 先在测试环境测试: 78 | 79 | ```bash 80 | twine upload --repository testpypi dist/* 81 | ``` 82 | 83 | 测试安装: 84 | 85 | ```bash 86 | pip install --index-url https://test.pypi.org/simple/ --no-deps xmu-rollcall-cli 87 | ``` 88 | 89 | ### 步骤 5: 上传到正式 PyPI 90 | 91 | 确认测试无误后,上传到正式 PyPI: 92 | 93 | ```bash 94 | twine upload dist/* 95 | ``` 96 | 97 | ### 步骤 6: 验证安装 98 | 99 | ```bash 100 | pip install xmu-rollcall-cli 101 | XMUrollcall-cli --help 102 | ``` 103 | 104 | ## 更新版本 105 | 106 | 每次更新时: 107 | 108 | 1. 修改版本号: 109 | - `xmu_rollcall/__init__.py` 中的 `__version__` 110 | - `setup.py` 中的 `version` 111 | - `pyproject.toml` 中的 `version` 112 | 113 | 2. 重复上述构建和上传步骤 114 | 115 | ## 快速上传脚本 116 | 117 | 可以创建一个 `release.sh` 脚本自动化发布流程: 118 | 119 | ```bash 120 | #!/bin/bash 121 | 122 | # 清理 123 | rm -rf build/ dist/ *.egg-info 124 | 125 | # 构建 126 | python -m build 127 | 128 | # 检查 129 | twine check dist/* 130 | 131 | # 上传 (可以选择 testpypi 或 pypi) 132 | echo "Upload to TestPyPI or PyPI?" 133 | echo "1) TestPyPI" 134 | echo "2) PyPI" 135 | read -p "Choose: " choice 136 | 137 | if [ "$choice" = "1" ]; then 138 | twine upload --repository testpypi dist/* 139 | elif [ "$choice" = "2" ]; then 140 | twine upload dist/* 141 | else 142 | echo "Invalid choice" 143 | fi 144 | ``` 145 | 146 | ## 常见问题 147 | 148 | ### Q: 包名已存在怎么办? 149 | 150 | A: PyPI 上的包名是唯一的。如果 `xmu-rollcall-cli` 已被占用,需要更换包名。 151 | 152 | ### Q: 版本号冲突 153 | 154 | A: 同一版本号只能上传一次。如需重新上传,必须增加版本号。 155 | 156 | ### Q: 忘记更新 README 157 | 158 | A: 可以更新版本号后重新上传。PyPI 会显示新版本的 README。 159 | 160 | ### Q: 如何删除已上传的版本? 161 | 162 | A: 登录 PyPI 网站,在项目管理页面可以删除特定版本(但不推荐)。 163 | 164 | ## 检查清单 165 | 166 | 上传前确认: 167 | 168 | - [ ] README.md 内容完整且格式正确 169 | - [ ] LICENSE 文件存在 170 | - [ ] 版本号已更新(三处) 171 | - [ ] MANIFEST.in 包含所有需要的文件 172 | - [ ] requirements.txt 依赖正确 173 | - [ ] 测试通过 174 | - [ ] 邮箱地址已填写(setup.py 和 pyproject.toml) 175 | - [ ] GitHub 仓库链接正确 176 | - [ ] 已在 TestPyPI 测试 177 | 178 | ## 有用的命令 179 | 180 | ```bash 181 | # 查看包信息 182 | python setup.py --version 183 | python setup.py --name 184 | python setup.py --classifiers 185 | 186 | # 查看将被包含的文件 187 | python setup.py sdist --dry-run 188 | 189 | # 本地安装测试 190 | pip install -e . 191 | 192 | # 卸载 193 | pip uninstall xmu-rollcall-cli 194 | ``` 195 | 196 | ## 参考资料 197 | 198 | - [Python 打包用户指南](https://packaging.python.org/) 199 | - [Twine 文档](https://twine.readthedocs.io/) 200 | - [PyPI 帮助](https://pypi.org/help/) 201 | 202 | -------------------------------------------------------------------------------- /legacy/v2.1.0/main.py: -------------------------------------------------------------------------------- 1 | # 厦大数字化教学平台自动签到机器人 V1.0 2 | # by KrsMt-0113 3 | import time 4 | import json 5 | import uuid 6 | import requests 7 | from tkinter import Tk, Label 8 | from PIL import ImageTk, Image 9 | from selenium import webdriver 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | from selenium.webdriver.support import expected_conditions as EC 13 | from selenium.webdriver.chrome.options import Options 14 | from parse_rollcalls import parse_rollcalls 15 | from get_config import get_config_path 16 | 17 | # 读取学号、密码、Server酱sendkey 18 | with open(get_config_path(), "r", encoding="utf-8") as f: 19 | config = json.load(f) 20 | username = config["username"] 21 | password = config["password"] 22 | 23 | # 签到列表获取接口,轮询间隔,轮询脚本 24 | api_url = "https://lnt.xmu.edu.cn/api/radar/rollcalls" 25 | interval = 1.5 26 | fetch_script = """ 27 | const url = arguments[0]; 28 | const callback = arguments[arguments.length - 1]; 29 | fetch(url, {credentials: 'include'}) 30 | .then(resp => resp.text().then(text => callback({status: resp.status, ok: resp.ok, text: text}))) 31 | .catch(err => callback({error: String(err)})); 32 | """ 33 | 34 | chrome_options = Options() 35 | # chrome_options.add_argument("--start-maximized") # 有头调试 36 | chrome_options.add_argument("--headless") # 无头运行 37 | 38 | # 启动selenium 39 | print("正在初始化...") 40 | driver = webdriver.Chrome(options=chrome_options) 41 | 42 | # 访问登录页面,不开VPN好像连不上 43 | driver.get("https://lnt.xmu.edu.cn") 44 | print("已连接到厦大数字化教学平台。") 45 | 46 | # 检查是否需要验证码,有验证码直接登录,否则扫码 47 | # 待优化: 提取验证码图片,OCR识别或用户自行登录 48 | ts = int(time.time() * 1000) 49 | temp_header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"} 50 | temp_url = f"https://ids.xmu.edu.cn/authserver/checkNeedCaptcha.htl?username={username}&_={ts}" 51 | res_data = requests.get(temp_url, cookies={c['name']: c['value'] for c in driver.get_cookies()}, headers=temp_header).json() 52 | if not res_data['isNeed']: 53 | print("登录无需验证,正在登录...") 54 | WebDriverWait(driver, 10).until( 55 | EC.element_to_be_clickable((By.ID, "userNameLogin_a")) 56 | ).click() 57 | driver.find_element(By.ID, "username").send_keys(username) 58 | driver.find_element(By.ID, "password").send_keys(password) 59 | driver.find_element(By.ID, "login_submit").click() 60 | time.sleep(1) 61 | else: 62 | print("账号密码登录暂时不可用,请用企业微信扫码登录。") 63 | driver.find_element(By.ID, "qrLogin_a").click() 64 | driver.set_window_size(1920, 1080) 65 | time.sleep(1) 66 | driver.get_screenshot_as_file("cache/fullpage.png") 67 | root = Tk() 68 | img = Image.open("cache/fullpage.png") 69 | tk_img = ImageTk.PhotoImage(img) 70 | Label(root, image=tk_img).pack() 71 | root.mainloop() 72 | 73 | res = requests.get(api_url, cookies={c['name']: c['value'] for c in driver.get_cookies()}) 74 | if res.status_code == 200: 75 | print("登录成功!五秒后进入监控...") 76 | else: 77 | print("登录失败。") 78 | driver.quit() 79 | time.sleep(5) 80 | exit(0) 81 | 82 | time.sleep(5) 83 | 84 | 85 | deviceID = uuid.uuid4() 86 | print(f"签到监控启动。") 87 | start = time.time() 88 | temp_data = {'rollcalls': []} 89 | while True: 90 | res = driver.execute_async_script(fetch_script, api_url) 91 | if res['status'] == 200: 92 | text = res.get('text', '') 93 | try: 94 | data = json.loads(text) 95 | if temp_data == data: 96 | continue 97 | else: 98 | temp_data = data 99 | if len(temp_data['rollcalls']) > 0: 100 | if not parse_rollcalls(temp_data, driver): 101 | temp_data = {'rollcalls': []} 102 | except Exception as e: 103 | print(time.strftime("%H:%M:%S", time.localtime()), ":发生错误") 104 | 105 | elif res['status'] != 200: 106 | print("失去连接,请重新登录。") 107 | break 108 | time.sleep(interval) 109 | 110 | driver.quit() 111 | -------------------------------------------------------------------------------- /legacy/v3.0.1/misc.py: -------------------------------------------------------------------------------- 1 | import os, time, requests, json 2 | from verify import send_code, send_radar 3 | 4 | base_url = "https://lnt.xmu.edu.cn" 5 | headers = { 6 | "User-Agent":( 7 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 8 | "AppleWebKit/537.36 (KHTML, like Gecko) " 9 | "Chrome/120.0.0.0 Safari/537.36" 10 | ) 11 | } 12 | 13 | def a(data, session): 14 | data_empty = {'rollcalls': []} 15 | result = p(data, session) 16 | if False in result: return data_empty 17 | else: return data 18 | 19 | def c(): 20 | # clear the console 21 | if os.name == 'nt': 22 | os.system('cls') 23 | else: 24 | os.system('clear') 25 | 26 | def d(data): 27 | rollcalls = data['rollcalls'] 28 | result = [] 29 | if rollcalls: 30 | rollcall_count = len(rollcalls) 31 | for rollcall in rollcalls: 32 | result.append( 33 | { 34 | 'course_title': rollcall['course_title'], 35 | 'created_by_name': rollcall['created_by_name'], 36 | 'department_name': rollcall['department_name'], 37 | 'is_expired': rollcall['is_expired'], 38 | 'is_number': rollcall['is_number'], 39 | 'is_radar': rollcall['is_radar'], 40 | 'rollcall_id': rollcall['rollcall_id'], 41 | 'rollcall_status': rollcall['rollcall_status'], 42 | 'scored': rollcall['scored'], 43 | 'status': rollcall['status'] 44 | } 45 | ) 46 | else: 47 | rollcall_count = 0 48 | return rollcall_count, result 49 | 50 | def p(data, session): 51 | count, rollcalls = d(data) 52 | answer_status = [False for _ in range(count)] 53 | if count: 54 | print(time.strftime("%H:%M:%S", time.localtime()), f"New rollcall(s) found!\n") 55 | for i in range(count): 56 | print(f"{i+1} of {count} :") 57 | print(f"Course name:{rollcalls[i]['course_title']},rollcall created by {rollcalls[i]['department_name']} {rollcalls[i]['created_by_name']}.") 58 | if rollcalls[i]['is_radar']: 59 | temp_str = "Radar rollcall" 60 | elif rollcalls[i]['is_number']: 61 | temp_str = "Number rollcall" 62 | else: 63 | temp_str = "QRcode rollcall" 64 | print(f"rollcall type:{temp_str}\n") 65 | if (rollcalls[i]['status'] == 'absent') & (rollcalls[i]['is_number']) & (not rollcalls[i]['is_radar']): 66 | if send_code(session, rollcalls[i]['rollcall_id']): 67 | answer_status[i] = True 68 | else: 69 | print("Answering failed.") 70 | elif rollcalls[i]['status'] == 'on_call_fine': 71 | print("Already answered.") 72 | answer_status[i] = True 73 | elif rollcalls[i]['is_radar']: 74 | if send_radar(session, rollcalls[i]['rollcall_id']): 75 | answer_status[i] = True 76 | else: 77 | print("Answering failed.") 78 | else: 79 | # todo: qrcode rollcall 80 | print("Answering failed.") 81 | return answer_status 82 | 83 | def t(name): 84 | if time.localtime().tm_hour < 12 and time.localtime().tm_hour >= 5: 85 | greeting = "Good morning" 86 | elif time.localtime().tm_hour < 18 and time.localtime().tm_hour >= 12: 87 | greeting = "Good afternoon" 88 | else: 89 | greeting = "Good evening" 90 | return f"{greeting}, {name}!" 91 | 92 | 93 | def s(sess: requests.Session, path: str): 94 | try: 95 | cj_divt = requests.utils.dict_from_cookiejar(sess.cookies) 96 | with open(path, "w", encoding="utf-8") as f: 97 | json.dump(cj_divt, f) 98 | except Exception: 99 | pass 100 | 101 | def l(sess: requests.Session, path: str): 102 | try: 103 | with open(path, "r", encoding="utf-8") as f: 104 | cj_dict = json.load(f) 105 | sess.cookies = requests.utils.cookiejar_from_dict(cj_dict) 106 | return True 107 | except Exception: 108 | return False 109 | 110 | def v(sess: requests.Session) -> dict: 111 | try: 112 | resp = sess.get(f"{base_url}/api/profile", headers=headers) 113 | if resp.status_code == 200: 114 | data = resp.json() 115 | if isinstance(data, dict) and "name" in data: 116 | return data 117 | except Exception: 118 | pass 119 | return {} -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/verify.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import time 3 | import asyncio 4 | import aiohttp 5 | import math 6 | from aiohttp import CookieJar 7 | 8 | base_url = "https://lnt.xmu.edu.cn" 9 | headers = { 10 | "User-Agent": ( 11 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 12 | "AppleWebKit/537.36 (KHTML, like Gecko) " 13 | "Chrome/120.0.0.0 Safari/537.36" 14 | ), 15 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 16 | "Accept-Language": "zh-CN,zh;q=0.9", 17 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 18 | } 19 | 20 | def pad(i): 21 | return str(i).zfill(4) 22 | 23 | def send_code(in_session, rollcall_id): 24 | url = f"{base_url}/api/rollcall/{rollcall_id}/answer_number_rollcall" 25 | print("Trying number code...") 26 | t00 = time.time() 27 | 28 | async def put_request(i, session, stop_flag, answer_url, sem, timeout): 29 | if stop_flag.is_set(): 30 | return None 31 | async with sem: 32 | if stop_flag.is_set(): 33 | return None 34 | payload = { 35 | "deviceId": str(uuid.uuid4()), 36 | "numberCode": pad(i) 37 | } 38 | try: 39 | async with session.put(answer_url, json=payload) as r: 40 | if r.status == 200: 41 | stop_flag.set() 42 | return pad(i) 43 | except Exception: 44 | pass 45 | return None 46 | 47 | async def main(): 48 | stop_flag = asyncio.Event() 49 | sem = asyncio.Semaphore(200) 50 | timeout = aiohttp.ClientTimeout(total=5) 51 | cookie_jar = CookieJar() 52 | for c in in_session.cookies: 53 | cookie_jar.update_cookies({c.name: c.value}) 54 | async with aiohttp.ClientSession(headers=in_session.headers, cookie_jar=cookie_jar) as session: 55 | tasks = [asyncio.create_task(put_request(i, session, stop_flag, url, sem, timeout)) for i in range(10000)] 56 | try: 57 | for coro in asyncio.as_completed(tasks): 58 | res = await coro 59 | if res is not None: 60 | for t in tasks: 61 | if not t.done(): 62 | t.cancel() 63 | print("Number code rollcall answered successfully.\nNumber code: ", res) 64 | time.sleep(5) 65 | t01 = time.time() 66 | print("Time: %.2f s." % (t01 - t00)) 67 | return True 68 | finally: 69 | for t in tasks: 70 | if not t.done(): 71 | t.cancel() 72 | await asyncio.gather(*tasks, return_exceptions=True) 73 | t01 = time.time() 74 | print("Failed.\nTime: %.2f s." % (t01 - t00)) 75 | return False 76 | 77 | return asyncio.run(main()) 78 | 79 | def send_radar(in_session, rollcall_id): 80 | url = f"{base_url}/api/rollcall/{rollcall_id}/answer" 81 | 82 | lat_1, lat_2 = 24.3, 24.6 83 | lon_1, lon_2 = 118.0, 118.2 84 | 85 | def payload(lat, lon): 86 | return { 87 | "accuracy": 35, 88 | "altitude": 0, 89 | "altitudeAccuracy": None, 90 | "deviceId": str(uuid.uuid4()), 91 | "heading": None, 92 | "latitude": lat, 93 | "longitude": lon, 94 | "speed": None 95 | } 96 | 97 | res_1 = in_session.put(url, json=payload(lat_1, lon_1), headers=headers) 98 | data_1 = res_1.json() 99 | res_2 = in_session.put(url, json=payload(lat_2, lon_2), headers=headers) 100 | data_2 = res_2.json() 101 | 102 | if res_1.status_code == 200 or res_2.status_code == 200: 103 | return True 104 | 105 | distance_1 = data_1.get("distance") 106 | distance_2 = data_2.get("distance") 107 | 108 | def latlon_to_xy(lat, lon, lat0, lon0): 109 | R = 6371000 110 | x = math.radians(lon - lon0) * R * math.cos(math.radians(lat0)) 111 | y = math.radians(lat - lat0) * R 112 | return x, y 113 | 114 | def xy_to_latlon(x, y, lat0, lon0): 115 | R = 6371000 116 | lat = lat0 + math.degrees(y / R) 117 | lon = lon0 + math.degrees(x / (R * math.cos(math.radians(lat0)))) 118 | return lat, lon 119 | 120 | def circle_intersections(x1, y1, d1, x2, y2, d2): 121 | D = math.hypot(x2 - x1, y2 - y1) 122 | 123 | if D > d1 + d2 or D < abs(d1 - d2): 124 | return None 125 | 126 | a = (d1**2 - d2**2 + D**2) / (2 * D) 127 | h = math.sqrt(d1**2 - a**2) 128 | 129 | xm = x1 + a * (x2 - x1) / D 130 | ym = y1 + a * (y2 - y1) / D 131 | 132 | rx = -(y2 - y1) * (h / D) 133 | ry = (x2 - x1) * (h / D) 134 | 135 | p1 = (xm + rx, ym + ry) 136 | p2 = (xm - rx, ym - ry) 137 | return p1, p2 138 | 139 | def solve_two_points(lat1, lon1, lat2, lon2, d1, d2): 140 | lat0 = (lat1 + lat2) / 2 141 | lon0 = (lon1 + lon2) / 2 142 | x1, y1 = latlon_to_xy(lat1, lon1, lat0, lon0) 143 | x2, y2 = latlon_to_xy(lat2, lon2, lat0, lon0) 144 | 145 | sols = circle_intersections(x1, y1, d1, x2, y2, d2) 146 | if sols is None: 147 | return None 148 | 149 | p1 = xy_to_latlon(sols[0][0], sols[0][1], lat0, lon0) 150 | p2 = xy_to_latlon(sols[1][0], sols[1][1], lat0, lon0) 151 | return p1, p2 152 | 153 | resolutions = solve_two_points(lat_1, lon_1, lat_2, lon_2, distance_1, distance_2) 154 | if resolutions: 155 | ((sol_x_1, sol_y_1), (sol_x_2, sol_y_2)) = resolutions 156 | else: 157 | return False 158 | 159 | payload_1 = payload(sol_x_1, sol_y_1) 160 | payload_2 = payload(sol_x_2, sol_y_2) 161 | 162 | res_3 = in_session.put(url, json=payload_1, headers=headers) 163 | if res_3.status_code == 200: 164 | return True 165 | else: 166 | print(res_3.json()) 167 | res_4 = in_session.put(url, json=payload_2, headers=headers) 168 | if res_4.status_code == 200: 169 | return True 170 | 171 | return False 172 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pathlib import Path 4 | 5 | def get_config_dir(): 6 | """ 7 | 获取配置目录路径,支持沙盒环境(如 a-Shell) 8 | 优先级: 9 | 1. 环境变量 XMU_ROLLCALL_CONFIG_DIR 10 | 2. 用户主目录下的 .xmu_rollcall(如果可访问) 11 | 3. 当前工作目录下的 .xmu_rollcall(沙盒环境备用方案) 12 | """ 13 | # 优先使用环境变量指定的路径 14 | if env_path := os.environ.get("XMU_ROLLCALL_CONFIG_DIR"): 15 | return Path(env_path) 16 | 17 | # 尝试使用用户主目录 18 | try: 19 | home_config_dir = Path.home() / ".xmu_rollcall" 20 | # 测试是否可以创建目录(检测沙盒权限) 21 | home_config_dir.mkdir(parents=True, exist_ok=True) 22 | # 测试是否可以写入文件 23 | test_file = home_config_dir / ".test_write" 24 | try: 25 | test_file.touch() 26 | test_file.unlink() 27 | return home_config_dir 28 | except (OSError, PermissionError): 29 | pass 30 | except (OSError, PermissionError, RuntimeError): 31 | pass 32 | 33 | # 降级到当前工作目录(适用于沙盒环境) 34 | return Path.cwd() / ".xmu_rollcall" 35 | 36 | CONFIG_DIR = get_config_dir() 37 | CONFIG_FILE = CONFIG_DIR / "config.json" 38 | 39 | DEFAULT_CONFIG = { 40 | "accounts": [], 41 | "current_account_id": None 42 | } 43 | 44 | DEFAULT_ACCOUNT = { 45 | "id": 0, 46 | "name": "", 47 | "username": "", 48 | "password": "" 49 | } 50 | 51 | def ensure_config_dir(): 52 | """确保配置目录存在""" 53 | try: 54 | CONFIG_DIR.mkdir(parents=True, exist_ok=True) 55 | except (OSError, PermissionError) as e: 56 | raise RuntimeError(f"无法创建配置目录 {CONFIG_DIR}: {e}\n提示:可以设置环境变量 XMU_ROLLCALL_CONFIG_DIR 指定配置目录位置") 57 | 58 | def load_config(): 59 | """加载配置文件""" 60 | ensure_config_dir() 61 | if CONFIG_FILE.exists(): 62 | try: 63 | with open(CONFIG_FILE, "r", encoding="utf-8") as f: 64 | config = json.load(f) 65 | # 兼容旧版配置格式 66 | if "accounts" not in config and "username" in config: 67 | # 迁移旧配置到新格式 68 | old_username = config.get("username", "") 69 | old_password = config.get("password", "") 70 | if old_username and old_password: 71 | new_config = { 72 | "accounts": [{ 73 | "id": 1, 74 | "name": "", 75 | "username": old_username, 76 | "password": old_password 77 | }], 78 | "current_account_id": 1 79 | } 80 | return new_config 81 | return DEFAULT_CONFIG.copy() 82 | return config 83 | except Exception: 84 | return DEFAULT_CONFIG.copy() 85 | return DEFAULT_CONFIG.copy() 86 | 87 | def save_config(config): 88 | """保存配置文件""" 89 | ensure_config_dir() 90 | with open(CONFIG_FILE, "w", encoding="utf-8") as f: 91 | json.dump(config, f, indent=2, ensure_ascii=False) 92 | 93 | def get_next_account_id(config): 94 | """获取下一个可用的账号ID""" 95 | accounts = config.get("accounts", []) 96 | if not accounts: 97 | return 1 98 | return max(acc.get("id", 0) for acc in accounts) + 1 99 | 100 | def add_account(config, username, password, name): 101 | """添加新账号""" 102 | account_id = get_next_account_id(config) 103 | new_account = { 104 | "id": account_id, 105 | "name": name, 106 | "username": username, 107 | "password": password 108 | } 109 | if "accounts" not in config: 110 | config["accounts"] = [] 111 | config["accounts"].append(new_account) 112 | # 如果是第一个账号,设为当前账号 113 | if config.get("current_account_id") is None: 114 | config["current_account_id"] = account_id 115 | return account_id 116 | 117 | def get_account_by_id(config, account_id): 118 | """通过ID获取账号""" 119 | for acc in config.get("accounts", []): 120 | if acc.get("id") == account_id: 121 | return acc 122 | return None 123 | 124 | def get_current_account(config): 125 | """获取当前选中的账号""" 126 | current_id = config.get("current_account_id") 127 | if current_id is None: 128 | return None 129 | return get_account_by_id(config, current_id) 130 | 131 | def set_current_account(config, account_id): 132 | """设置当前账号""" 133 | config["current_account_id"] = account_id 134 | 135 | def get_all_accounts(config): 136 | """获取所有账号""" 137 | return config.get("accounts", []) 138 | 139 | def is_config_complete(config): 140 | """检查配置是否完整(至少有一个账号且已选择当前账号)""" 141 | current_account = get_current_account(config) 142 | if current_account is None: 143 | return False 144 | required_fields = ["username", "password"] 145 | return all(current_account.get(field) for field in required_fields) 146 | 147 | def get_cookies_path(account_id=None): 148 | """获取cookies文件路径,根据账号ID命名""" 149 | ensure_config_dir() 150 | if account_id is None: 151 | config = load_config() 152 | account_id = config.get("current_account_id", 1) 153 | return str(CONFIG_DIR / f"{account_id}.json") 154 | 155 | def delete_account(config, account_id): 156 | """ 157 | 删除账号并重新编号 158 | 返回: (成功删除, 被删除账号的旧cookies路径列表, 需要重命名的cookies映射) 159 | """ 160 | import os 161 | 162 | accounts = config.get("accounts", []) 163 | 164 | # 找到要删除的账号 165 | account_to_delete = None 166 | delete_index = -1 167 | for i, acc in enumerate(accounts): 168 | if acc.get("id") == account_id: 169 | account_to_delete = acc 170 | delete_index = i 171 | break 172 | 173 | if account_to_delete is None: 174 | return False, [], {} 175 | 176 | # 记录需要删除的cookies文件 177 | cookies_to_delete = get_cookies_path(account_id) 178 | 179 | # 记录需要重命名的cookies文件 (旧ID -> 新ID) 180 | cookies_to_rename = {} 181 | 182 | # 删除账号 183 | accounts.pop(delete_index) 184 | 185 | # 重新编号:所有ID大于被删除ID的账号需要向前移动 186 | for acc in accounts: 187 | old_id = acc.get("id") 188 | if old_id > account_id: 189 | new_id = old_id - 1 190 | old_cookies = get_cookies_path(old_id) 191 | new_cookies = get_cookies_path(new_id) 192 | if os.path.exists(old_cookies): 193 | cookies_to_rename[old_cookies] = new_cookies 194 | acc["id"] = new_id 195 | 196 | # 更新当前账号ID 197 | current_id = config.get("current_account_id") 198 | if current_id == account_id: 199 | # 删除的是当前账号,切换到第一个账号 200 | if accounts: 201 | config["current_account_id"] = accounts[0].get("id") 202 | else: 203 | config["current_account_id"] = None 204 | elif current_id is not None and current_id > account_id: 205 | # 当前账号ID需要减1 206 | config["current_account_id"] = current_id - 1 207 | 208 | config["accounts"] = accounts 209 | 210 | return True, cookies_to_delete, cookies_to_rename 211 | 212 | def perform_account_deletion(cookies_to_delete, cookies_to_rename): 213 | """执行cookies文件的删除和重命名操作""" 214 | import os 215 | 216 | # 删除被删除账号的cookies 217 | if os.path.exists(cookies_to_delete): 218 | os.remove(cookies_to_delete) 219 | 220 | # 重命名其他账号的cookies文件(按顺序处理,避免覆盖) 221 | # 先按旧ID从小到大排序 222 | sorted_renames = sorted(cookies_to_rename.items(), key=lambda x: x[0]) 223 | for old_path, new_path in sorted_renames: 224 | if os.path.exists(old_path): 225 | # 如果目标文件已存在,先删除 226 | if os.path.exists(new_path): 227 | os.remove(new_path) 228 | os.rename(old_path, new_path) 229 | 230 | -------------------------------------------------------------------------------- /legacy/v3.0.1/verify.py: -------------------------------------------------------------------------------- 1 | import uuid, time, asyncio, aiohttp, os, sys, math 2 | from aiohttp import CookieJar 3 | 4 | base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 5 | file_path = os.path.join(base_dir, "info.txt") 6 | base_url = "https://lnt.xmu.edu.cn" 7 | headers = { 8 | "User-Agent": ( 9 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 10 | "AppleWebKit/537.36 (KHTML, like Gecko) " 11 | "Chrome/120.0.0.0 Safari/537.36" 12 | ), 13 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 14 | "Accept-Language": "zh-CN,zh;q=0.9", 15 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 16 | } 17 | 18 | # LATITUDE = 24.5 19 | # LONGITUDE = 118.1 20 | 21 | def pad(i): 22 | return str(i).zfill(4) 23 | 24 | def send_code(in_session, rollcall_id): 25 | url = f"{base_url}/api/rollcall/{rollcall_id}/answer_number_rollcall" 26 | print("Trying number code...") 27 | t00 = time.time() 28 | 29 | async def put_request(i, session, stop_flag, answer_url, sem, timeout): 30 | if stop_flag.is_set(): 31 | return None 32 | async with sem: 33 | if stop_flag.is_set(): 34 | return None 35 | payload = { 36 | "deviceId": str(uuid.uuid4()), 37 | "numberCode": pad(i) 38 | } 39 | try: 40 | async with session.put(answer_url, json=payload) as r: 41 | if r.status == 200: 42 | stop_flag.set() 43 | return pad(i) 44 | except Exception: 45 | pass 46 | return None 47 | 48 | async def main(): 49 | stop_flag = asyncio.Event() 50 | sem = asyncio.Semaphore(200) 51 | timeout = aiohttp.ClientTimeout(total=5) 52 | cookie_jar = CookieJar() 53 | for c in in_session.cookies: 54 | cookie_jar.update_cookies({c.name: c.value}) 55 | async with aiohttp.ClientSession(headers=in_session.headers, cookie_jar=cookie_jar) as session: 56 | tasks = [asyncio.create_task(put_request(i, session, stop_flag, url, sem, timeout)) for i in range(10000)] 57 | try: 58 | for coro in asyncio.as_completed(tasks): 59 | res = await coro 60 | if res is not None: 61 | for t in tasks: 62 | if not t.done(): 63 | t.cancel() 64 | print("Number code rollcall answered successfully.\nNumber code: ", res) 65 | time.sleep(5) 66 | t01 = time.time() 67 | print("Time: %.2f s." % (t01 - t00)) 68 | return True 69 | finally: 70 | # 确保所有 task 结束 71 | for t in tasks: 72 | if not t.done(): 73 | t.cancel() 74 | await asyncio.gather(*tasks, return_exceptions=True) 75 | t01 = time.time() 76 | print("Failed.\nTime: %.2f s." % (t01 - t00)) 77 | return False 78 | 79 | return asyncio.run(main()) 80 | 81 | # def send_radar(in_session, rollcall_id): 82 | # url = f"{base_url}/api/rollcall/{rollcall_id}/answer" 83 | # payload = { 84 | # "accuracy": 35, 85 | # "altitude": 0, 86 | # "altitudeAccuracy": None, 87 | # "deviceId": str(uuid.uuid4()), 88 | # "heading": None, 89 | # "latitude": LATITUDE, 90 | # "longitude": LONGITUDE, 91 | # "speed": None 92 | # } 93 | # res = in_session.put(url, json=payload, headers=headers) 94 | # if res.status_code == 200: 95 | # print("Radar rollcall answered successfully.") 96 | # return True 97 | # return False 98 | 99 | def send_radar(in_session, rollcall_id): 100 | url = f"{base_url}/api/rollcall/{rollcall_id}/answer" 101 | 102 | lat_1, lat_2 = 24.3, 24.6 103 | lon_1, lon_2 = 118.0, 118.2 104 | 105 | def payload(lat, lon): 106 | return { 107 | "accuracy": 35, 108 | "altitude": 0, 109 | "altitudeAccuracy": None, 110 | "deviceId": str(uuid.uuid4()), 111 | "heading": None, 112 | "latitude": lat, 113 | "longitude": lon, 114 | "speed": None 115 | } 116 | 117 | res_1 = in_session.put(url, json=payload(lat_1, lon_1), headers=headers) 118 | data_1 = res_1.json() 119 | res_2 = in_session.put(url, json=payload(lat_2, lon_2), headers=headers) 120 | data_2 = res_2.json() 121 | distance_1 = data_1["distance"] 122 | distance_2 = data_2["distance"] 123 | 124 | print(data_1, data_2) # debug 125 | 126 | if res_1.status_code == 200 or res_2.status_code == 200: 127 | print("[Debug] 预设位置签到成功。") 128 | time.sleep(10) 129 | return True 130 | 131 | def latlon_to_xy(lat, lon, lat0, lon0): 132 | R = 6371000 133 | x = math.radians(lon - lon0) * R * math.cos(math.radians(lat0)) 134 | y = math.radians(lat - lat0) * R 135 | return x, y 136 | 137 | def xy_to_latlon(x, y, lat0, lon0): 138 | R = 6371000 139 | lat = lat0 + math.degrees(y / R) 140 | lon = lon0 + math.degrees(x / (R * math.cos(math.radians(lat0)))) 141 | return lat, lon 142 | 143 | def circle_intersections(x1, y1, d1, x2, y2, d2): 144 | D = math.hypot(x2 - x1, y2 - y1) 145 | 146 | if D > d1 + d2 or D < abs(d1 - d2): 147 | return None 148 | 149 | a = (d1**2 - d2**2 + D**2) / (2 * D) 150 | h = math.sqrt(d1**2 - a**2) 151 | 152 | xm = x1 + a * (x2 - x1) / D 153 | ym = y1 + a * (y2 - y1) / D 154 | 155 | rx = -(y2 - y1) * (h / D) 156 | ry = (x2 - x1) * (h / D) 157 | 158 | p1 = (xm + rx, ym + ry) 159 | p2 = (xm - rx, ym - ry) 160 | return p1, p2 161 | 162 | def solve_two_points(lat1, lon1, lat2, lon2, d1, d2): 163 | lat0 = (lat1 + lat2) / 2 164 | lon0 = (lon1 + lon2) / 2 165 | x1, y1 = latlon_to_xy(lat1, lon1, lat0, lon0) 166 | x2, y2 = latlon_to_xy(lat2, lon2, lat0, lon0) 167 | 168 | sols = circle_intersections(x1, y1, d1, x2, y2, d2) 169 | if sols is None: 170 | return None 171 | 172 | p1 = xy_to_latlon(sols[0][0], sols[0][1], lat0, lon0) 173 | p2 = xy_to_latlon(sols[1][0], sols[1][1], lat0, lon0) 174 | return p1, p2 175 | 176 | resolutions = solve_two_points(lat_1, lon_1, lat_2, lon_2, distance_1, distance_2) 177 | if resolutions: 178 | ((sol_x_1, sol_y_1), (sol_x_2, sol_y_2)) = resolutions 179 | print(f"[Debug] 解方程得到:{resolutions}") # debug 180 | else: 181 | print("[Debug] 无解。") # debug 182 | time.sleep(10) # debug 183 | return False 184 | 185 | payload_1 = payload(sol_x_1, sol_y_1) 186 | payload_2 = payload(sol_x_2, sol_y_2) 187 | 188 | res_3 = in_session.put(url, json=payload_1, headers=headers) 189 | if res_3.status_code == 200: 190 | print(f"[Debug] 解 1 签到成功。对应经纬度:{sol_x_1, sol_y_1}") # debug 191 | print(res_3.json()) # debug 192 | time.sleep(10) # debug 193 | return True 194 | else: 195 | print(res_3.json()) 196 | res_4 = in_session.put(url, json=payload_2, headers=headers) 197 | if res_4.status_code == 200: 198 | print(f"[Debug] 解 2 签到成功。对应经纬度:{sol_x_2, sol_y_2}") # debug 199 | print(res_4.json()) # debug 200 | time.sleep(10) # debug 201 | return True 202 | print(res_4.json()) # debug 203 | 204 | print("[Debug] 失败了。") # debug 205 | time.sleep(10) # debug 206 | return False 207 | -------------------------------------------------------------------------------- /legacy/v2.1.0/main_gui.py: -------------------------------------------------------------------------------- 1 | # 厦大数字化教学平台自动签到机器人 V1.0 - GUI版本 2 | # by KrsMt-0113 3 | import sys 4 | import time 5 | import json 6 | import uuid 7 | import requests 8 | from PyQt6.QtWidgets import QApplication 9 | from PyQt6.QtCore import QThread, pyqtSignal, QObject 10 | from selenium import webdriver 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.webdriver.chrome.options import Options 15 | from parse_rollcalls import parse_rollcalls 16 | from gui import MainWindow 17 | from get_config import get_config_path 18 | 19 | 20 | class WorkerSignals(QObject): 21 | """工作线程信号""" 22 | log = pyqtSignal(str, str) # message, level 23 | status = pyqtSignal(str) 24 | qr_code = pyqtSignal(str) # image path 25 | hide_qr = pyqtSignal() 26 | started = pyqtSignal() 27 | finished = pyqtSignal() 28 | check_increment = pyqtSignal() 29 | sign_increment = pyqtSignal() 30 | 31 | 32 | class MonitorWorker(QThread): 33 | """监控工作线程""" 34 | 35 | def __init__(self, username, password, sendkey): 36 | super().__init__() 37 | self.username = username 38 | self.password = password 39 | self.sendkey = sendkey 40 | self.signals = WorkerSignals() 41 | self.running = True 42 | self.driver = None 43 | 44 | def log(self, message, level="info"): 45 | """发送日志信号""" 46 | self.signals.log.emit(message, level) 47 | 48 | def update_status(self, status): 49 | """更新状态""" 50 | self.signals.status.emit(status) 51 | 52 | def run(self): 53 | """运行监控任务""" 54 | try: 55 | # 签到列表获取接口,轮询间隔,轮询脚本 56 | api_url = "https://lnt.xmu.edu.cn/api/radar/rollcalls" 57 | interval = 1.5 58 | fetch_script = """ 59 | const url = arguments[0]; 60 | const callback = arguments[arguments.length - 1]; 61 | fetch(url, {credentials: 'include'}) 62 | .then(resp => resp.text().then(text => callback({status: resp.status, ok: resp.ok, text: text}))) 63 | .catch(err => callback({error: String(err)})); 64 | """ 65 | 66 | chrome_options = Options() 67 | chrome_options.add_argument("--headless") # 无头运行 68 | 69 | # 启动selenium 70 | self.log("Initializing Selenium...", "info") 71 | self.update_status("Initializing...") 72 | self.driver = webdriver.Chrome(options=chrome_options) 73 | 74 | # 访问登录页面 75 | self.driver.get("https://lnt.xmu.edu.cn") 76 | self.log("Connected to XMU Course platform.", "success") 77 | 78 | # 检查是否需要验证码 79 | ts = int(time.time() * 1000) 80 | temp_header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"} 81 | temp_url = f"https://ids.xmu.edu.cn/authserver/checkNeedCaptcha.htl?username={self.username}&_={ts}" 82 | res_data = requests.get(temp_url, cookies={c['name']: c['value'] for c in self.driver.get_cookies()}, headers=temp_header).json() 83 | 84 | if not res_data['isNeed']: 85 | self.log("No captcha needed. Logging you in...", "info") 86 | self.update_status("Logging in...") 87 | # sc_send(self.sendkey, "签到机器人", "账号密码登录中...", {"tags": "签到机器人"}) 88 | 89 | WebDriverWait(self.driver, 10).until( 90 | EC.element_to_be_clickable((By.ID, "userNameLogin_a")) 91 | ).click() 92 | self.driver.find_element(By.ID, "username").send_keys(self.username) 93 | self.driver.find_element(By.ID, "password").send_keys(self.password) 94 | self.driver.find_element(By.ID, "login_submit").click() 95 | time.sleep(1) 96 | else: 97 | self.log("Captcha needed. Please login in through QR code.", "warning") 98 | self.update_status("Waiting for QR code login...") 99 | # sc_send(self.sendkey, "签到机器人", "需要图形验证码,请扫码登录。", {"tags": "签到机器人"}) 100 | 101 | # 切换到二维码登录 102 | self.driver.find_element(By.ID, "qrLogin_a").click() 103 | self.driver.set_window_size(1920, 1080) 104 | time.sleep(1) 105 | 106 | # 截图并显示二维码 107 | elem = self.driver.find_element(By.ID, "qr_img") 108 | elem.screenshot("cache/qr_code.png") 109 | self.signals.qr_code.emit("cache/qr_code.png") 110 | 111 | # 等待登录成功(轮询检查) 112 | login_success = False 113 | for _ in range(120): # 最多等待2分钟 114 | if not self.running: 115 | return 116 | try: 117 | res = requests.get(api_url, cookies={c['name']: c['value'] for c in self.driver.get_cookies()}) 118 | if res.status_code == 200: 119 | login_success = True 120 | break 121 | except: 122 | pass 123 | time.sleep(1) 124 | 125 | self.signals.hide_qr.emit() 126 | 127 | if not login_success: 128 | self.log("Login timeout.", "error") 129 | return 130 | 131 | # 验证登录 132 | time.sleep(3) 133 | res = requests.get(api_url, cookies={c['name']: c['value'] for c in self.driver.get_cookies()}) 134 | if res.status_code == 200: 135 | self.log("Successfully login!", "success") 136 | # sc_send(self.sendkey, "签到机器人", "登录成功!五秒后进入监控模式...", {"tags": "签到机器人"}) 137 | else: 138 | self.log("Login failed.", "error") 139 | return 140 | 141 | time.sleep(5) 142 | 143 | deviceID = uuid.uuid4() 144 | self.log("Start monitoring.", "success") 145 | self.update_status("Monitoring...") 146 | # sc_send(self.sendkey, "签到机器人", "签到监控已启动。", {"tags": "签到机器人"}) 147 | self.signals.started.emit() 148 | 149 | temp_data = {'rollcalls': []} 150 | check_count = 0 151 | 152 | while self.running: 153 | res = self.driver.execute_async_script(fetch_script, api_url) 154 | check_count += 1 155 | 156 | if check_count % 10 == 0: # 每10次检测更新一次计数 157 | self.signals.check_increment.emit() 158 | 159 | if res['status'] == 200: 160 | text = res.get('text', '') 161 | try: 162 | data = json.loads(text) 163 | if temp_data == data: 164 | continue 165 | else: 166 | temp_data = data 167 | if len(temp_data['rollcalls']) > 0: 168 | self.log(f"Find {len(temp_data['rollcalls'])} new rollcalls!", "warning") 169 | self.signals.sign_increment.emit() 170 | 171 | # 显示详细信息 172 | for idx, rollcall in enumerate(temp_data['rollcalls']): 173 | self.log(f"Rollcall {idx+1}/{len(temp_data['rollcalls'])}: {rollcall['course_title']}", "info") 174 | self.log(f" Launch by: {rollcall['created_by_name']}", "info") 175 | self.log(f" Status: {rollcall['rollcall_status']}", "info") 176 | 177 | if not parse_rollcalls(temp_data, self.driver): 178 | temp_data = {'rollcalls': []} 179 | else: 180 | self.log("Rollcall done.", "success") 181 | except Exception as e: 182 | self.log(f"Error: {str(e)}", "error") 183 | 184 | elif res['status'] != 200: 185 | self.log("Disconnected, monitor has stopped.", "error") 186 | # sc_send(self.sendkey, "签到机器人", "失去连接,监控已终止。", {"tags": "签到机器人"}) 187 | break 188 | 189 | time.sleep(interval) 190 | 191 | except Exception as e: 192 | self.log(f"Error: {str(e)}", "error") 193 | finally: 194 | if self.driver: 195 | self.driver.quit() 196 | self.signals.finished.emit() 197 | 198 | def stop(self): 199 | """停止监控""" 200 | self.running = False 201 | if self.driver: 202 | try: 203 | self.driver.quit() 204 | except: 205 | pass 206 | 207 | 208 | def main(): 209 | """主函数""" 210 | # 读取配置 211 | try: 212 | with open(get_config_path(), "r", encoding="utf-8") as f: 213 | config = json.load(f) 214 | username = config["username"] 215 | password = config["password"] 216 | except Exception as e: 217 | print(f"读取配置文件失败: {e}") 218 | return 219 | 220 | # 创建应用 221 | app = QApplication(sys.argv) 222 | 223 | # 创建主窗口 224 | window = MainWindow() 225 | 226 | # 创建工作线程 227 | worker = MonitorWorker(username, password, sendkey="") # sendkey留空表示不发送通知 228 | 229 | # 连接信号 230 | worker.signals.log.connect(window.add_log) 231 | worker.signals.status.connect(window.update_status) 232 | worker.signals.qr_code.connect(window.show_qr_code) 233 | worker.signals.hide_qr.connect(window.hide_qr_code) 234 | worker.signals.started.connect(window.start_monitoring) 235 | worker.signals.finished.connect(window.stop_monitoring) 236 | worker.signals.check_increment.connect(window.increment_check_count) 237 | worker.signals.sign_increment.connect(window.increment_sign_count) 238 | 239 | # 连接停止按钮 240 | window.stop_button.clicked.connect(worker.stop) 241 | window.stop_button.clicked.connect(lambda: window.add_log("Monitor stopped by user.", "warning")) 242 | 243 | # 启动工作线程 244 | worker.start() 245 | 246 | # 显示窗口 247 | window.show() 248 | 249 | # 运行应用 250 | sys.exit(app.exec()) 251 | 252 | 253 | if __name__ == "__main__": 254 | main() 255 | -------------------------------------------------------------------------------- /legacy/v2.1.0/gui.py: -------------------------------------------------------------------------------- 1 | # GUI界面模块 2 | from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 3 | QLabel, QPushButton, QTextEdit, QFrame, QGraphicsDropShadowEffect) 4 | from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QThread 5 | from PyQt6.QtGui import QFont, QColor, QPalette, QPixmap 6 | import datetime 7 | 8 | 9 | class SignalEmitter(QThread): 10 | """用于线程安全地发送信号的辅助类""" 11 | log_signal = pyqtSignal(str, str) # message, level 12 | status_signal = pyqtSignal(str) 13 | qr_signal = pyqtSignal(str) # qr code path 14 | 15 | def __init__(self): 16 | super().__init__() 17 | 18 | 19 | class MainWindow(QMainWindow): 20 | def __init__(self): 21 | super().__init__() 22 | self.setWindowTitle("XMU RollCall Bot v2.0") 23 | self.setMinimumSize(600, 600) 24 | 25 | # 设置主题颜色 26 | self.primary_color = "#4A90E2" 27 | self.success_color = "#5CB85C" 28 | self.warning_color = "#F0AD4E" 29 | self.danger_color = "#D9534F" 30 | self.bg_color = "#F5F7FA" 31 | self.card_color = "#FFFFFF" 32 | self.text_color = "#333333" 33 | self.text_muted = "#999999" 34 | 35 | self.setup_ui() 36 | 37 | def setup_ui(self): 38 | """设置UI布局""" 39 | # 设置窗口背景色 40 | self.setStyleSheet(f""" 41 | QMainWindow {{ 42 | background-color: {self.bg_color}; 43 | }} 44 | """) 45 | 46 | # 主窗口部件 47 | central_widget = QWidget() 48 | self.setCentralWidget(central_widget) 49 | 50 | # 主布局 51 | main_layout = QVBoxLayout(central_widget) 52 | main_layout.setSpacing(20) 53 | main_layout.setContentsMargins(20, 20, 20, 20) 54 | 55 | # 标题栏 56 | title_layout = QHBoxLayout() 57 | title_label = QLabel("RollCall Monitor") 58 | title_label.setFont(QFont("Monaco", 24, QFont.Weight.Bold)) 59 | title_label.setStyleSheet(f"color: {self.text_color};") 60 | title_layout.addWidget(title_label) 61 | title_layout.addStretch() 62 | 63 | # 状态指示器 64 | self.status_indicator = QLabel("●") 65 | self.status_indicator.setFont(QFont("Monaco", 20)) 66 | self.status_indicator.setStyleSheet(f"color: {self.text_muted};") 67 | title_layout.addWidget(self.status_indicator) 68 | 69 | self.status_label = QLabel("Initializing...") 70 | self.status_label.setFont(QFont("Monaco", 12)) 71 | self.status_label.setStyleSheet(f"color: {self.text_muted};") 72 | title_layout.addWidget(self.status_label) 73 | 74 | main_layout.addLayout(title_layout) 75 | 76 | # 信息卡片区域 77 | info_frame = self.create_card() 78 | info_layout = QHBoxLayout(info_frame) 79 | info_layout.setSpacing(30) 80 | 81 | # 监控时长 82 | self.time_widget = self.create_info_widget("⏱️", "Running", "00:00:00") 83 | info_layout.addWidget(self.time_widget) 84 | 85 | # 检测次数 86 | self.check_widget = self.create_info_widget("🔍", "Queries", "0") 87 | info_layout.addWidget(self.check_widget) 88 | 89 | # 签到次数 90 | self.sign_widget = self.create_info_widget("✅", "Success", "0") 91 | info_layout.addWidget(self.sign_widget) 92 | 93 | main_layout.addWidget(info_frame) 94 | 95 | # 日志区域 96 | log_label = QLabel("Logs") 97 | log_label.setFont(QFont("Monaco", 14, QFont.Weight.Bold)) 98 | log_label.setStyleSheet(f"color: {self.text_color};") 99 | main_layout.addWidget(log_label) 100 | 101 | self.log_text = QTextEdit() 102 | self.log_text.setReadOnly(True) 103 | self.log_text.setStyleSheet(f""" 104 | QTextEdit {{ 105 | background-color: {self.card_color}; 106 | border: 2px solid #E1E8ED; 107 | border-radius: 10px; 108 | padding: 15px; 109 | font-family: 'Monaco', monospace; 110 | font-size: 12px; 111 | color: {self.text_color}; 112 | }} 113 | """) 114 | main_layout.addWidget(self.log_text, stretch=1) 115 | 116 | # 二维码显示区域(初始隐藏) 117 | self.qr_frame = self.create_card() 118 | qr_layout = QVBoxLayout(self.qr_frame) 119 | qr_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 120 | 121 | qr_title = QLabel("📱 Sign in with QR Code by WeCom.") 122 | qr_title.setFont(QFont("Monaco", 14, QFont.Weight.Bold)) 123 | qr_title.setStyleSheet(f"color: {self.text_color};") 124 | qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter) 125 | qr_layout.addWidget(qr_title) 126 | 127 | self.qr_label = QLabel() 128 | self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 129 | self.qr_label.setStyleSheet("padding: 20px;") 130 | qr_layout.addWidget(self.qr_label) 131 | 132 | self.qr_frame.hide() 133 | main_layout.addWidget(self.qr_frame) 134 | 135 | # 底部按钮 136 | button_layout = QHBoxLayout() 137 | button_layout.addStretch() 138 | 139 | self.stop_button = QPushButton(" Stop Monitoring ") 140 | self.stop_button.setEnabled(False) 141 | self.stop_button.setFixedSize(140, 40) 142 | self.stop_button.setStyleSheet(f""" 143 | QPushButton {{ 144 | background-color: {self.danger_color}; 145 | color: white; 146 | border: none; 147 | border-radius: 8px; 148 | font-family: 'Monaco', monospace; 149 | font-size: 13px; 150 | font-weight: bold; 151 | }} 152 | QPushButton:hover {{ 153 | background-color: #C9302C; 154 | }} 155 | QPushButton:disabled {{ 156 | background-color: #CCCCCC; 157 | color: #666666; 158 | }} 159 | """) 160 | button_layout.addWidget(self.stop_button) 161 | 162 | main_layout.addLayout(button_layout) 163 | 164 | # 初始化计数器 165 | self.check_count = 0 166 | self.sign_count = 0 167 | self.start_time = None 168 | 169 | # 定时器更新运行时间 170 | self.timer = QTimer() 171 | self.timer.timeout.connect(self.update_runtime) 172 | 173 | def create_card(self): 174 | """创建卡片容器""" 175 | frame = QFrame() 176 | frame.setStyleSheet(f""" 177 | QFrame {{ 178 | background-color: {self.card_color}; 179 | border-radius: 15px; 180 | border: none; 181 | }} 182 | """) 183 | # 添加阴影效果 184 | shadow = QGraphicsDropShadowEffect() 185 | shadow.setBlurRadius(20) 186 | shadow.setColor(QColor(0, 0, 0, 30)) 187 | shadow.setOffset(0, 3) 188 | frame.setGraphicsEffect(shadow) 189 | return frame 190 | 191 | def create_info_widget(self, icon, title, value): 192 | """创建信息显示部件""" 193 | widget = QWidget() 194 | layout = QVBoxLayout(widget) 195 | layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 196 | layout.setSpacing(5) 197 | 198 | icon_label = QLabel(icon) 199 | icon_label.setFont(QFont("Monaco", 32)) 200 | icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 201 | layout.addWidget(icon_label) 202 | 203 | value_label = QLabel(value) 204 | value_label.setFont(QFont("Monaco", 24, QFont.Weight.Bold)) 205 | value_label.setStyleSheet(f"color: {self.primary_color};") 206 | value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 207 | value_label.setObjectName("value") 208 | layout.addWidget(value_label) 209 | 210 | title_label = QLabel(title) 211 | title_label.setFont(QFont("Monaco", 11)) 212 | title_label.setStyleSheet(f"color: {self.text_muted};") 213 | title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 214 | layout.addWidget(title_label) 215 | 216 | widget.setMinimumWidth(180) 217 | return widget 218 | 219 | def add_log(self, message, level="info"): 220 | """添加日志信息""" 221 | timestamp = datetime.datetime.now().strftime("%H:%M:%S") 222 | 223 | color = self.text_color 224 | prefix = "ℹ️" 225 | 226 | if level == "success": 227 | color = self.success_color 228 | prefix = "✅" 229 | elif level == "warning": 230 | color = self.warning_color 231 | prefix = "⚠️" 232 | elif level == "error": 233 | color = self.danger_color 234 | prefix = "❌" 235 | 236 | html = f'[{timestamp}] {prefix} {message}' 237 | self.log_text.append(html) 238 | 239 | # 自动滚动到底部 240 | scrollbar = self.log_text.verticalScrollBar() 241 | scrollbar.setValue(scrollbar.maximum()) 242 | 243 | def update_status(self, status, color=None): 244 | """更新状态""" 245 | self.status_label.setText(status) 246 | if color is None: 247 | if "Monitoring..." in status: 248 | color = self.success_color 249 | elif "Failed" in status or "Error" in status: 250 | color = self.danger_color 251 | elif "Initializing..." in status or "Logging in" in status or "login" in status: 252 | color = self.warning_color 253 | else: 254 | color = self.text_muted 255 | 256 | self.status_label.setStyleSheet(f"color: {color};") 257 | self.status_indicator.setStyleSheet(f"color: {color};") 258 | 259 | def show_qr_code(self, image_path): 260 | """显示二维码""" 261 | pixmap = QPixmap(image_path) 262 | if not pixmap.isNull(): 263 | # 缩放到合适大小 264 | scaled_pixmap = pixmap.scaled(400, 400, Qt.AspectRatioMode.KeepAspectRatio, 265 | Qt.TransformationMode.SmoothTransformation) 266 | self.qr_label.setPixmap(scaled_pixmap) 267 | self.qr_frame.show() 268 | 269 | def hide_qr_code(self): 270 | """隐藏二维码""" 271 | self.qr_frame.hide() 272 | 273 | def start_monitoring(self): 274 | """开始监控""" 275 | self.start_time = datetime.datetime.now() 276 | self.timer.start(1000) # 每秒更新一次 277 | self.stop_button.setEnabled(True) 278 | self.update_status("Monitoring...") 279 | 280 | def stop_monitoring(self): 281 | """停止监控""" 282 | self.timer.stop() 283 | self.stop_button.setEnabled(False) 284 | self.update_status("Stopped") 285 | 286 | def update_runtime(self): 287 | """更新运行时间""" 288 | if self.start_time: 289 | delta = datetime.datetime.now() - self.start_time 290 | hours, remainder = divmod(int(delta.total_seconds()), 3600) 291 | minutes, seconds = divmod(remainder, 60) 292 | time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" 293 | 294 | time_value = self.time_widget.findChild(QLabel, "value") 295 | if time_value: 296 | time_value.setText(time_str) 297 | 298 | def increment_check_count(self): 299 | """增加检测次数""" 300 | self.check_count += 1 301 | check_value = self.check_widget.findChild(QLabel, "value") 302 | if check_value: 303 | check_value.setText(str(self.check_count)) 304 | 305 | def increment_sign_count(self): 306 | """增加签到次数""" 307 | self.sign_count += 1 308 | sign_value = self.sign_widget.findChild(QLabel, "value") 309 | if sign_value: 310 | sign_value.setText(str(self.sign_count)) 311 | 312 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import sys 3 | from xmulogin import xmulogin 4 | from .config import ( 5 | load_config, save_config, is_config_complete, get_cookies_path, 6 | add_account, get_all_accounts, get_current_account, set_current_account, 7 | get_account_by_id, CONFIG_FILE, delete_account, perform_account_deletion 8 | ) 9 | from .monitor import start_monitor, base_url, headers 10 | 11 | # ANSI Color codes 12 | class Colors: 13 | HEADER = '\033[95m' 14 | OKBLUE = '\033[94m' 15 | OKCYAN = '\033[96m' 16 | OKGREEN = '\033[92m' 17 | WARNING = '\033[93m' 18 | FAIL = '\033[91m' 19 | ENDC = '\033[0m' 20 | BOLD = '\033[1m' 21 | GRAY = '\033[90m' 22 | 23 | @click.group(invoke_without_command=True) 24 | @click.pass_context 25 | def cli(ctx): 26 | if ctx.invoked_subcommand is None: 27 | click.echo(f"{Colors.OKCYAN}{Colors.BOLD}XMU Rollcall Bot CLI v3.1.5{Colors.ENDC}") 28 | click.echo(f"\nUsage:") 29 | click.echo(f" xmu config Configure credentials and add accounts") 30 | click.echo(f" xmu switch Switch between accounts") 31 | click.echo(f" xmu start Start monitoring rollcalls") 32 | click.echo(f" xmu refresh Refresh the login status") 33 | click.echo(f" xmu --help Show this message") 34 | 35 | @cli.command() 36 | def config(): 37 | """配置账号:添加、删除账号""" 38 | click.echo(f"\n{Colors.BOLD}{Colors.OKCYAN}=== XMU Rollcall Configuration ==={Colors.ENDC}\n") 39 | 40 | current_config = load_config() 41 | 42 | def show_accounts(): 43 | """显示账号列表""" 44 | accounts = get_all_accounts(current_config) 45 | if accounts: 46 | click.echo(f"{Colors.BOLD}Existing accounts:{Colors.ENDC}") 47 | current_account = get_current_account(current_config) 48 | for acc in accounts: 49 | current_marker = f" {Colors.OKGREEN}(current){Colors.ENDC}" if current_account and acc.get("id") == current_account.get("id") else "" 50 | click.echo(f" {acc.get('id')}: {acc.get('name') or acc.get('username')}{current_marker}") 51 | click.echo() 52 | else: 53 | click.echo(f"{Colors.GRAY}No accounts configured.{Colors.ENDC}\n") 54 | 55 | def add_new_account(): 56 | """添加新账号""" 57 | click.echo(f"{Colors.BOLD}Adding a new account...{Colors.ENDC}\n") 58 | 59 | # 输入新账号信息 60 | username = click.prompt(f"{Colors.BOLD}Username{Colors.ENDC}") 61 | password = click.prompt(f"{Colors.BOLD}Password{Colors.ENDC}", hide_input=False) 62 | 63 | # 验证登录 64 | click.echo(f"\n{Colors.OKCYAN}Validating credentials...{Colors.ENDC}") 65 | try: 66 | session = xmulogin(type=3, username=username, password=password) 67 | if session: 68 | click.echo(f"{Colors.OKGREEN}✓ Login successful!{Colors.ENDC}") 69 | 70 | # 获取用户姓名 71 | click.echo(f"{Colors.OKCYAN}Fetching user profile...{Colors.ENDC}") 72 | try: 73 | profile = session.get(f"{base_url}/api/profile", headers=headers).json() 74 | name = profile.get("name", "") 75 | click.echo(f"{Colors.OKGREEN}✓ Welcome, {name}!{Colors.ENDC}") 76 | except Exception: 77 | click.echo(f"{Colors.WARNING}⚠ Could not fetch profile, using username as name{Colors.ENDC}") 78 | name = username 79 | 80 | # 添加账号 81 | try: 82 | account_id = add_account(current_config, username, password, name) 83 | save_config(current_config) 84 | 85 | click.echo(f"{Colors.OKGREEN}✓ Account added successfully! (ID: {account_id}){Colors.ENDC}") 86 | click.echo(f"{Colors.GRAY}Configuration file: {CONFIG_FILE}{Colors.ENDC}\n") 87 | except RuntimeError as e: 88 | click.echo(f"{Colors.FAIL}✗ Failed to save configuration: {str(e)}{Colors.ENDC}") 89 | click.echo(f"{Colors.WARNING}Tip: In sandboxed environments (like a-Shell), set environment variable:{Colors.ENDC}") 90 | click.echo(f" export XMU_ROLLCALL_CONFIG_DIR=~/Documents/.xmu_rollcall") 91 | else: 92 | click.echo(f"{Colors.FAIL}✗ Login failed. Please check your credentials.{Colors.ENDC}") 93 | except Exception as e: 94 | click.echo(f"{Colors.FAIL}✗ Error during login validation: {str(e)}{Colors.ENDC}") 95 | 96 | def delete_existing_account(): 97 | """删除账号""" 98 | accounts = get_all_accounts(current_config) 99 | if not accounts: 100 | click.echo(f"{Colors.WARNING}No accounts to delete.{Colors.ENDC}\n") 101 | return 102 | 103 | show_accounts() 104 | 105 | # 让用户选择要删除的账号 106 | valid_ids = [str(acc.get("id")) for acc in accounts] 107 | selected_id = click.prompt( 108 | f"{Colors.BOLD}Enter account ID to delete{Colors.ENDC}", 109 | type=click.Choice(valid_ids, case_sensitive=False) 110 | ) 111 | 112 | selected_id = int(selected_id) 113 | selected_account = get_account_by_id(current_config, selected_id) 114 | 115 | if selected_account: 116 | # 确认删除 117 | confirm = click.prompt( 118 | f"{Colors.WARNING}Are you sure you want to delete account '{selected_account.get('name') or selected_account.get('username')}' (ID: {selected_id})?{Colors.ENDC}", 119 | type=click.Choice(['y', 'n'], case_sensitive=False), 120 | default='n' 121 | ) 122 | 123 | if confirm.lower() == 'y': 124 | # 执行删除 125 | success, cookies_to_delete, cookies_to_rename = delete_account(current_config, selected_id) 126 | 127 | if success: 128 | # 保存配置 129 | save_config(current_config) 130 | 131 | # 处理cookies文件 132 | perform_account_deletion(cookies_to_delete, cookies_to_rename) 133 | 134 | click.echo(f"{Colors.OKGREEN}✓ Account deleted successfully!{Colors.ENDC}") 135 | 136 | # 显示ID变更提示 137 | if cookies_to_rename: 138 | click.echo(f"{Colors.GRAY}Note: Account IDs have been re-assigned.{Colors.ENDC}") 139 | click.echo() 140 | else: 141 | click.echo(f"{Colors.FAIL}✗ Failed to delete account.{Colors.ENDC}\n") 142 | else: 143 | click.echo(f"{Colors.GRAY}Deletion cancelled.{Colors.ENDC}\n") 144 | else: 145 | click.echo(f"{Colors.FAIL}✗ Account not found.{Colors.ENDC}\n") 146 | 147 | # 主循环 148 | while True: 149 | show_accounts() 150 | 151 | click.echo(f"{Colors.BOLD}Choose an action:{Colors.ENDC}") 152 | click.echo(f" {Colors.OKCYAN}n{Colors.ENDC} - Add new account") 153 | click.echo(f" {Colors.OKCYAN}d{Colors.ENDC} - Delete account") 154 | click.echo(f" {Colors.OKCYAN}q{Colors.ENDC} - Quit") 155 | 156 | action = click.prompt( 157 | f"\n{Colors.BOLD}Action{Colors.ENDC}", 158 | type=click.Choice(['n', 'd', 'q'], case_sensitive=False), 159 | default='q' 160 | ) 161 | 162 | click.echo() 163 | 164 | if action.lower() == 'n': 165 | add_new_account() 166 | elif action.lower() == 'd': 167 | delete_existing_account() 168 | elif action.lower() == 'q': 169 | # 退出前显示最终账号列表 170 | accounts = get_all_accounts(current_config) 171 | if accounts: 172 | click.echo(f"{Colors.BOLD}Final account list:{Colors.ENDC}") 173 | current_account = get_current_account(current_config) 174 | for acc in accounts: 175 | current_marker = f" {Colors.OKGREEN}(current){Colors.ENDC}" if current_account and acc.get("id") == current_account.get("id") else "" 176 | click.echo(f" {acc.get('id')}: {acc.get('name') or acc.get('username')}{current_marker}") 177 | click.echo(f"\n{Colors.GRAY}You can run: {Colors.BOLD}xmu switch{Colors.ENDC} to switch between accounts") 178 | click.echo(f"{Colors.GRAY}You can run: {Colors.BOLD}xmu start{Colors.ENDC} to start monitoring") 179 | break 180 | 181 | @cli.command() 182 | def start(): 183 | """启动签到监控""" 184 | # 加载配置 185 | config_data = load_config() 186 | 187 | # 检查配置是否完整 188 | if not is_config_complete(config_data): 189 | click.echo(f"{Colors.FAIL}✗ Configuration incomplete!{Colors.ENDC}") 190 | click.echo(f"Please run: {Colors.BOLD}xmu config{Colors.ENDC}") 191 | sys.exit(1) 192 | 193 | # 获取当前账号 194 | current_account = get_current_account(config_data) 195 | click.echo(f"{Colors.OKCYAN}Using account: {current_account.get('name') or current_account.get('username')} (ID: {current_account.get('id')}){Colors.ENDC}") 196 | 197 | # 启动监控 198 | try: 199 | start_monitor(current_account) 200 | except KeyboardInterrupt: 201 | click.echo(f"\n{Colors.WARNING}Shutting down...{Colors.ENDC}") 202 | sys.exit(0) 203 | except Exception as e: 204 | click.echo(f"\n{Colors.FAIL}Error: {str(e)}{Colors.ENDC}") 205 | sys.exit(1) 206 | 207 | @cli.command() 208 | def refresh(): 209 | """清除当前账号的登录缓存""" 210 | config_data = load_config() 211 | current_account = get_current_account(config_data) 212 | 213 | if not current_account: 214 | click.echo(f"{Colors.FAIL}✗ No account configured!{Colors.ENDC}") 215 | click.echo(f"Please run: {Colors.BOLD}xmu config{Colors.ENDC}") 216 | sys.exit(1) 217 | 218 | account_id = current_account.get("id") 219 | cookies_path = get_cookies_path(account_id) 220 | try: 221 | click.echo(f"\n{Colors.WARNING}Deleting cookies for account {account_id} ({current_account.get('name')})...{Colors.ENDC}") 222 | # delete cookies file 223 | import os 224 | if os.path.exists(cookies_path): 225 | os.remove(cookies_path) 226 | click.echo(f"{Colors.OKGREEN}✓ Cookies deleted successfully.{Colors.ENDC}") 227 | else: 228 | click.echo(f"{Colors.GRAY}No cookies file found to delete.{Colors.ENDC}") 229 | sys.exit(0) 230 | except Exception as e: 231 | click.echo(f"{Colors.FAIL}✗ Failed to delete cookies: {str(e)}{Colors.ENDC}") 232 | sys.exit(1) 233 | 234 | 235 | @cli.command() 236 | def switch(): 237 | """切换当前使用的账号""" 238 | click.echo(f"\n{Colors.BOLD}{Colors.OKCYAN}=== Switch Account ==={Colors.ENDC}\n") 239 | 240 | config_data = load_config() 241 | accounts = get_all_accounts(config_data) 242 | 243 | if not accounts: 244 | click.echo(f"{Colors.FAIL}✗ No accounts configured!{Colors.ENDC}") 245 | click.echo(f"Please run: {Colors.BOLD}xmu config{Colors.ENDC}") 246 | sys.exit(1) 247 | 248 | current_account = get_current_account(config_data) 249 | current_id = current_account.get("id") if current_account else None 250 | 251 | # 显示账号列表 252 | click.echo(f"{Colors.BOLD}Available accounts:{Colors.ENDC}") 253 | for acc in accounts: 254 | current_marker = f" {Colors.OKGREEN}(current){Colors.ENDC}" if acc.get("id") == current_id else "" 255 | click.echo(f" {acc.get('id')}: {acc.get('name') or acc.get('username')}{current_marker}") 256 | 257 | click.echo() 258 | 259 | # 让用户选择账号 260 | valid_ids = [str(acc.get("id")) for acc in accounts] 261 | selected_id = click.prompt( 262 | f"{Colors.BOLD}Enter account ID to switch to{Colors.ENDC}", 263 | type=click.Choice(valid_ids, case_sensitive=False) 264 | ) 265 | 266 | selected_id = int(selected_id) 267 | selected_account = get_account_by_id(config_data, selected_id) 268 | 269 | if selected_account: 270 | set_current_account(config_data, selected_id) 271 | save_config(config_data) 272 | click.echo(f"\n{Colors.OKGREEN}✓ Switched to account: {selected_account.get('name') or selected_account.get('username')} (ID: {selected_id}){Colors.ENDC}") 273 | click.echo(f"{Colors.GRAY}You can now run: {Colors.BOLD}xmu start{Colors.ENDC}") 274 | else: 275 | click.echo(f"{Colors.FAIL}✗ Account not found!{Colors.ENDC}") 276 | sys.exit(1) 277 | 278 | 279 | if __name__ == '__main__': 280 | cli() 281 | 282 | -------------------------------------------------------------------------------- /xmu-rollcall-cli/xmu_rollcall/monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | import sys 4 | import requests 5 | import shutil 6 | import re 7 | from xmulogin import xmulogin 8 | from .utils import clear_screen, save_session, load_session, verify_session 9 | from .rollcall_handler import process_rollcalls 10 | from .config import get_cookies_path 11 | 12 | base_url = "https://lnt.xmu.edu.cn" 13 | interval = 1 14 | headers = { 15 | "User-Agent": ( 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 17 | "AppleWebKit/537.36 (KHTML, like Gecko) " 18 | "Chrome/120.0.0.0 Safari/537.36" 19 | ), 20 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 21 | "Accept-Language": "zh-CN,zh;q=0.9", 22 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 23 | } 24 | 25 | # ANSI Color codes 26 | class Colors: 27 | __slots__ = () 28 | HEADER = '\033[95m' 29 | OKBLUE = '\033[94m' 30 | OKCYAN = '\033[96m' 31 | OKGREEN = '\033[92m' 32 | WARNING = '\033[93m' 33 | FAIL = '\033[91m' 34 | ENDC = '\033[0m' 35 | BOLD = '\033[1m' 36 | UNDERLINE = '\033[4m' 37 | GRAY = '\033[90m' 38 | WHITE = '\033[97m' 39 | BG_BLUE = '\033[44m' 40 | BG_GREEN = '\033[42m' 41 | BG_CYAN = '\033[46m' 42 | 43 | BOLD_LABEL = f"{Colors.BOLD}" 44 | CYAN_TEXT = f"{Colors.OKCYAN}" 45 | GREEN_TEXT = f"{Colors.OKGREEN}" 46 | YELLOW_TEXT = f"{Colors.WARNING}" 47 | END = Colors.ENDC 48 | 49 | def get_terminal_width(): 50 | """获取终端宽度""" 51 | try: 52 | return shutil.get_terminal_size().columns 53 | except: 54 | return 80 55 | 56 | _ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 57 | 58 | def strip_ansi(text): 59 | """移除ANSI颜色代码以计算实际文本长度""" 60 | return _ANSI_ESCAPE.sub('', text) 61 | 62 | def center_text(text, width=None): 63 | """居中文本""" 64 | if width is None: 65 | width = get_terminal_width() 66 | text_len = len(strip_ansi(text)) 67 | if text_len >= width: 68 | return text 69 | left_padding = (width - text_len) // 2 70 | return ' ' * left_padding + text 71 | 72 | def print_banner(): 73 | """打印美化的横幅""" 74 | width = get_terminal_width() 75 | line = '=' * width 76 | 77 | title1 = "XMU Rollcall Bot CLI" 78 | title2 = "Version 3.1.6" 79 | 80 | print(f"{Colors.OKCYAN}{line}{Colors.ENDC}") 81 | print(center_text(f"{Colors.BOLD}{title1}{Colors.ENDC}")) 82 | print(center_text(f"{Colors.GRAY}{title2}{Colors.ENDC}")) 83 | print(f"{Colors.OKCYAN}{line}{Colors.ENDC}") 84 | 85 | def print_separator(char="-"): 86 | """打印分隔线""" 87 | width = get_terminal_width() 88 | print(f"{Colors.GRAY}{char * width}{Colors.ENDC}") 89 | 90 | def format_time(seconds): 91 | """格式化时间显示""" 92 | hours = seconds // 3600 93 | minutes = (seconds % 3600) // 60 94 | secs = seconds % 60 95 | if hours > 0: 96 | return f"{hours}h {minutes}m {secs}s" 97 | elif minutes > 0: 98 | return f"{minutes}m {secs}s" 99 | else: 100 | return f"{secs}s" 101 | 102 | _COLOR_PALETTE = ( 103 | Colors.FAIL, 104 | Colors.WARNING, 105 | Colors.OKGREEN, 106 | Colors.OKCYAN, 107 | Colors.OKBLUE, 108 | Colors.HEADER 109 | ) 110 | _COLOR_COUNT = len(_COLOR_PALETTE) 111 | 112 | def get_colorful_text(text, color_offset=0): 113 | """为文本的每个字符应用不同的颜色""" 114 | return ''.join( 115 | _COLOR_PALETTE[(i + color_offset) % _COLOR_COUNT] + char 116 | for i, char in enumerate(text) 117 | ) + Colors.ENDC 118 | 119 | def print_footer_text(color_offset=0): 120 | """打印底部彩色文字""" 121 | text = "XMU-Rollcall-Bot @ KrsMt" 122 | colored = get_colorful_text(text, color_offset) 123 | print(center_text(colored)) 124 | 125 | def print_dashboard(name, start_time, query_count, banner_frame=0, show_banner=True): 126 | """打印主仪表板""" 127 | clear_screen() 128 | print_banner() 129 | 130 | local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 131 | 132 | if time.localtime().tm_hour < 12 and time.localtime().tm_hour >= 5: 133 | greeting = "Good morning" 134 | elif time.localtime().tm_hour < 18 and time.localtime().tm_hour >= 12: 135 | greeting = "Good afternoon" 136 | else: 137 | greeting = "Good evening" 138 | 139 | now = time.time() 140 | running_time = int(now - start_time) 141 | 142 | print(f"\n{Colors.OKGREEN}{Colors.BOLD}{greeting}, {name}!{Colors.ENDC}\n") 143 | 144 | print(f"{Colors.BOLD}SYSTEM STATUS{Colors.ENDC}") 145 | print_separator() 146 | print(f"{Colors.BOLD}Current Time:{Colors.ENDC} {Colors.OKCYAN}{local_time}{Colors.ENDC}") 147 | print(f"{Colors.BOLD}Running Time:{Colors.ENDC} {Colors.OKGREEN}{format_time(running_time)}{Colors.ENDC}") 148 | print(f"{Colors.BOLD}Query Count:{Colors.ENDC} {Colors.WARNING}{query_count}{Colors.ENDC}") 149 | 150 | print(f"\n{Colors.BOLD}ROLLCALL MONITOR{Colors.ENDC}") 151 | print_separator() 152 | print(f"{Colors.OKGREEN}Status:{Colors.ENDC} Active - Monitoring for new rollcalls...") 153 | print(f"{Colors.GRAY}Checking every {interval} second(s){Colors.ENDC}") 154 | print(f"{Colors.GRAY}Press Ctrl+C to exit{Colors.ENDC}\n") 155 | print_separator() 156 | 157 | if show_banner: 158 | print() 159 | print_footer_text(banner_frame) 160 | 161 | def print_login_status(message, is_success=True): 162 | """打印登录状态""" 163 | if is_success: 164 | print(f"{Colors.OKGREEN}[SUCCESS]{Colors.ENDC} {message}") 165 | else: 166 | print(f"{Colors.FAIL}[FAILED]{Colors.ENDC} {message}") 167 | 168 | TIME_LINE = 10 169 | RUNTIME_LINE = 11 170 | QUERY_LINE = 12 171 | FOOTER_LINE = 20 172 | 173 | def update_status_line(line_num, label, value, color): 174 | """更新指定行的状态信息,不清屏""" 175 | sys.stdout.write("\033[?25l") 176 | sys.stdout.write("\033[s") 177 | sys.stdout.write(f"\033[{line_num};0H") 178 | sys.stdout.write("\033[2K") 179 | sys.stdout.write(f"{Colors.BOLD}{label}{Colors.ENDC} {color}{value}{Colors.ENDC}") 180 | sys.stdout.write("\033[u") 181 | sys.stdout.write("\033[?25h") 182 | sys.stdout.flush() 183 | 184 | def update_footer_text(): 185 | """更新底部彩色文字,不清屏""" 186 | text = "XMU-Rollcall-Bot @ KrsMt" 187 | colored = get_colorful_text(text, 0) 188 | width = get_terminal_width() 189 | 190 | sys.stdout.write("\033[?25l") 191 | sys.stdout.write("\033[s") 192 | sys.stdout.write(f"\033[{FOOTER_LINE};0H") 193 | sys.stdout.write("\033[2K") 194 | 195 | text_len = len(text) 196 | left_padding = (width - text_len) // 2 197 | sys.stdout.write(' ' * left_padding + colored) 198 | 199 | sys.stdout.write("\033[u") 200 | sys.stdout.write("\033[?25h") 201 | sys.stdout.flush() 202 | 203 | def start_monitor(account): 204 | """启动监控程序""" 205 | USERNAME = account['username'] 206 | PASSWORD = account['password'] 207 | ACCOUNT_ID = account.get('id', 1) 208 | ACCOUNT_NAME = account.get('name', '') 209 | # LATITUDE = account.get('latitude', 0) 210 | # LONGITUDE = account.get('longitude', 0) 211 | 212 | # 设置全局位置信息 213 | # set_location(LATITUDE, LONGITUDE) 214 | 215 | cookies_path = get_cookies_path(ACCOUNT_ID) 216 | rollcalls_url = f"{base_url}/api/radar/rollcalls" 217 | session = None 218 | 219 | # 初始化 220 | clear_screen() 221 | print_banner() 222 | print(f"\n{Colors.BOLD}Initializing XMU Rollcall Bot...{Colors.ENDC}\n") 223 | print_separator() 224 | 225 | print(f"\n{Colors.OKCYAN}[Step 1/3]{Colors.ENDC} Checking credentials...") 226 | 227 | if os.path.exists(cookies_path): 228 | print(f"{Colors.OKCYAN}[Step 2/3]{Colors.ENDC} Found cached session, attempting to restore...") 229 | session_candidate = requests.Session() 230 | if load_session(session_candidate, cookies_path): 231 | profile = verify_session(session_candidate) 232 | if profile: 233 | session = session_candidate 234 | print_login_status("Session restored successfully", True) 235 | else: 236 | print_login_status("Session expired, will re-login", False) 237 | else: 238 | print_login_status("Failed to load session", False) 239 | 240 | if not session: 241 | print(f"{Colors.OKCYAN}[Step 2/3]{Colors.ENDC} Logging in with credentials...") 242 | time.sleep(2) 243 | session = xmulogin(type=3, username=USERNAME, password=PASSWORD) 244 | if session: 245 | save_session(session, cookies_path) 246 | print_login_status("Login successful", True) 247 | else: 248 | print_login_status("Login failed. Please check your credentials", False) 249 | time.sleep(5) 250 | sys.exit(1) 251 | 252 | print(f"{Colors.OKCYAN}[Step 3/3]{Colors.ENDC} Fetching user profile...") 253 | # profile = session.get(f"{base_url}/api/profile", headers=headers).json() 254 | # name = profile["name"] 255 | print_login_status(f"Welcome, {ACCOUNT_NAME}", True) 256 | 257 | print(f"\n{Colors.OKGREEN}{Colors.BOLD}Initialization complete{Colors.ENDC}") 258 | print(f"\n{Colors.GRAY}Starting monitor in 3 seconds...{Colors.ENDC}") 259 | time.sleep(3) 260 | 261 | # 主循环 262 | temp_data = {'rollcalls': []} 263 | query_count = 0 264 | start_time = time.time() 265 | 266 | print_dashboard(ACCOUNT_NAME, start_time, query_count, 0, show_banner=False) 267 | 268 | footer_initialized = False 269 | _last_query_time = 0 270 | 271 | try: 272 | while True: 273 | try: 274 | time.sleep(0.1) 275 | except KeyboardInterrupt: 276 | raise 277 | 278 | try: 279 | current_time = time.time() 280 | 281 | if not footer_initialized: 282 | footer_initialized = True 283 | update_footer_text() 284 | 285 | elapsed = int(current_time - start_time) 286 | if elapsed > _last_query_time: 287 | _last_query_time = elapsed 288 | data = session.get(rollcalls_url, headers=headers).json() 289 | query_count += 1 290 | 291 | local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 292 | running_time = format_time(elapsed) 293 | 294 | update_status_line(TIME_LINE, "Current Time:", local_time, Colors.OKCYAN) 295 | update_status_line(RUNTIME_LINE, "Running Time:", running_time, Colors.OKGREEN) 296 | update_status_line(QUERY_LINE, "Query Count: ", str(query_count), Colors.WARNING) 297 | 298 | if temp_data != data: 299 | temp_data = data 300 | if len(temp_data['rollcalls']) > 0: 301 | clear_screen() 302 | width = get_terminal_width() 303 | print(f"\n{Colors.WARNING}{Colors.BOLD}{'!' * width}{Colors.ENDC}") 304 | print(center_text(f"{Colors.WARNING}{Colors.BOLD}NEW ROLLCALL DETECTED{Colors.ENDC}")) 305 | print(f"{Colors.WARNING}{Colors.BOLD}{'!' * width}{Colors.ENDC}\n") 306 | temp_data = process_rollcalls(temp_data, session) 307 | print_separator("=") 308 | print(f"\n{center_text(f'{Colors.GRAY}Press Ctrl+C to exit, continuing monitor...{Colors.ENDC}')}\n") 309 | try: 310 | time.sleep(3) 311 | except KeyboardInterrupt: 312 | raise 313 | print_dashboard(ACCOUNT_NAME, start_time, query_count, 0) 314 | except KeyboardInterrupt: 315 | raise 316 | except Exception as e: 317 | clear_screen() 318 | print(f"\n{center_text(f'{Colors.FAIL}{Colors.BOLD}Error occurred:{Colors.ENDC} {str(e)}')}") 319 | print(f"{center_text(f'{Colors.GRAY}Exiting...{Colors.ENDC}')}\n") 320 | sys.exit(1) 321 | except KeyboardInterrupt: 322 | clear_screen() 323 | print(f"\n{center_text(f'{Colors.WARNING}Shutting down gracefully...{Colors.ENDC}')}") 324 | print(f"{center_text(f'{Colors.GRAY}Total queries performed: {query_count}{Colors.ENDC}')}") 325 | print(f"{center_text(f'{Colors.GRAY}Total running time: {format_time(int(time.time() - start_time))}{Colors.ENDC}')}") 326 | print(f"\n{center_text(f'{Colors.OKGREEN}Goodbye{Colors.ENDC}')}\n") 327 | sys.exit(0) 328 | 329 | -------------------------------------------------------------------------------- /legacy/v3.0.1/main_new.py: -------------------------------------------------------------------------------- 1 | # "I love vibe coding." - KrsMt 2 | 3 | import time, os, sys, requests, shutil 4 | from xmulogin import xmulogin 5 | from misc import c, a, l, v, s 6 | 7 | base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 8 | file_path = os.path.join(base_dir, "info.txt") 9 | cookies = os.path.join(base_dir, "cookies.json") 10 | 11 | with open(file_path, "r", encoding="utf-8") as f: 12 | lines = f.readlines() 13 | USERNAME = lines[0].strip() 14 | pwd = lines[1].strip() 15 | 16 | interval = 1 17 | headers = { 18 | "User-Agent": ( 19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 20 | "AppleWebKit/537.36 (KHTML, like Gecko) " 21 | "Chrome/120.0.0.0 Safari/537.36" 22 | ), 23 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 24 | "Accept-Language": "zh-CN,zh;q=0.9", 25 | "Referer": "https://ids.xmu.edu.cn/authserver/login", 26 | } 27 | base_url = "https://lnt.xmu.edu.cn" 28 | session = None 29 | rollcalls_url = f"{base_url}/api/radar/rollcalls" 30 | 31 | # ANSI Color codes - 使用 __slots__ 减少内存占用 32 | class Colors: 33 | __slots__ = () 34 | HEADER = '\033[95m' 35 | OKBLUE = '\033[94m' 36 | OKCYAN = '\033[96m' 37 | OKGREEN = '\033[92m' 38 | WARNING = '\033[93m' 39 | FAIL = '\033[91m' 40 | ENDC = '\033[0m' 41 | BOLD = '\033[1m' 42 | UNDERLINE = '\033[4m' 43 | GRAY = '\033[90m' 44 | WHITE = '\033[97m' 45 | BG_BLUE = '\033[44m' 46 | BG_GREEN = '\033[42m' 47 | BG_CYAN = '\033[46m' 48 | 49 | # 预编译常用的颜色组合,避免重复拼接 50 | BOLD_LABEL = f"{Colors.BOLD}" 51 | CYAN_TEXT = f"{Colors.OKCYAN}" 52 | GREEN_TEXT = f"{Colors.OKGREEN}" 53 | YELLOW_TEXT = f"{Colors.WARNING}" 54 | END = Colors.ENDC 55 | 56 | def get_terminal_width(): 57 | """获取终端宽度""" 58 | try: 59 | return shutil.get_terminal_size().columns 60 | except: 61 | return 80 # 默认宽度 62 | 63 | # 预编译正则表达式,避免每次调用时重新编译 64 | import re 65 | _ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 66 | 67 | def strip_ansi(text): 68 | """移除ANSI颜色代码以计算实际文本长度""" 69 | return _ANSI_ESCAPE.sub('', text) 70 | 71 | def center_text(text, width=None): 72 | """居中文本""" 73 | if width is None: 74 | width = get_terminal_width() 75 | text_len = len(strip_ansi(text)) 76 | if text_len >= width: 77 | return text 78 | left_padding = (width - text_len) // 2 79 | return ' ' * left_padding + text 80 | 81 | def print_banner(): 82 | """打印美化的横幅""" 83 | width = get_terminal_width() 84 | line = '=' * width 85 | 86 | title1 = "XMU Rollcall Bot CLI" 87 | title2 = "Version 3.1.0" 88 | 89 | print(f"{Colors.OKCYAN}{line}{Colors.ENDC}") 90 | print(center_text(f"{Colors.BOLD}{title1}{Colors.ENDC}")) 91 | print(center_text(f"{Colors.GRAY}{title2}{Colors.ENDC}")) 92 | print(f"{Colors.OKCYAN}{line}{Colors.ENDC}") 93 | 94 | def print_separator(char="-"): 95 | """打印分隔线""" 96 | width = get_terminal_width() 97 | print(f"{Colors.GRAY}{char * width}{Colors.ENDC}") 98 | 99 | def format_time(seconds): 100 | """格式化时间显示""" 101 | hours = seconds // 3600 102 | minutes = (seconds % 3600) // 60 103 | secs = seconds % 60 104 | if hours > 0: 105 | return f"{hours}h {minutes}m {secs}s" 106 | elif minutes > 0: 107 | return f"{minutes}m {secs}s" 108 | else: 109 | return f"{secs}s" 110 | 111 | 112 | 113 | # 预定义颜色列表,避免每次调用时重新创建 114 | _COLOR_PALETTE = ( 115 | Colors.FAIL, # 红色 116 | Colors.WARNING, # 黄色 117 | Colors.OKGREEN, # 绿色 118 | Colors.OKCYAN, # 青色 119 | Colors.OKBLUE, # 蓝色 120 | Colors.HEADER # 紫色 121 | ) 122 | _COLOR_COUNT = len(_COLOR_PALETTE) 123 | 124 | def get_colorful_text(text, color_offset=0): 125 | """为文本的每个字符应用不同的颜色(优化版)""" 126 | # 使用列表推导式和 join,比字符串拼接更高效 127 | return ''.join( 128 | _COLOR_PALETTE[(i + color_offset) % _COLOR_COUNT] + char 129 | for i, char in enumerate(text) 130 | ) + Colors.ENDC 131 | 132 | def print_footer_text(color_offset=0): 133 | """打印底部彩色文字""" 134 | text = "XMU-Rollcall-Bot @ KrsMt" 135 | colored = get_colorful_text(text, color_offset) 136 | print(center_text(colored)) 137 | 138 | 139 | def print_dashboard(name, start_time, query_count, banner_frame=0, show_banner=True): 140 | """打印主仪表板""" 141 | c() 142 | print_banner() 143 | 144 | # 获取当前时间 145 | local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 146 | 147 | # 获取问候语 148 | if time.localtime().tm_hour < 12 and time.localtime().tm_hour >= 5: 149 | greeting = "Good morning" 150 | elif time.localtime().tm_hour < 18 and time.localtime().tm_hour >= 12: 151 | greeting = "Good afternoon" 152 | else: 153 | greeting = "Good evening" 154 | 155 | # 运行时间 156 | now = time.time() 157 | running_time = int(now - start_time) 158 | 159 | # 显示用户信息 160 | print(f"\n{Colors.OKGREEN}{Colors.BOLD}{greeting}, {name}!{Colors.ENDC}\n") 161 | 162 | # 显示系统状态 163 | print(f"{Colors.BOLD}SYSTEM STATUS{Colors.ENDC}") 164 | print_separator() 165 | print(f"{Colors.BOLD}Current Time:{Colors.ENDC} {Colors.OKCYAN}{local_time}{Colors.ENDC}") 166 | print(f"{Colors.BOLD}Running Time:{Colors.ENDC} {Colors.OKGREEN}{format_time(running_time)}{Colors.ENDC}") 167 | print(f"{Colors.BOLD}Query Count:{Colors.ENDC} {Colors.WARNING}{query_count}{Colors.ENDC}") 168 | 169 | # 显示监控状态 170 | print(f"\n{Colors.BOLD}ROLLCALL MONITOR{Colors.ENDC}") 171 | print_separator() 172 | print(f"{Colors.OKGREEN}Status:{Colors.ENDC} Active - Monitoring for new rollcalls...") 173 | print(f"{Colors.GRAY}Checking every {interval} second(s){Colors.ENDC}") 174 | print(f"{Colors.GRAY}Press Ctrl+C to exit{Colors.ENDC}\n") 175 | print_separator() 176 | 177 | # 显示底部彩色文字 178 | if show_banner: 179 | print() 180 | print_footer_text(banner_frame) 181 | 182 | 183 | 184 | def print_login_status(message, is_success=True): 185 | """打印登录状态""" 186 | if is_success: 187 | print(f"{Colors.OKGREEN}[SUCCESS]{Colors.ENDC} {message}") 188 | else: 189 | print(f"{Colors.FAIL}[FAILED]{Colors.ENDC} {message}") 190 | 191 | # 初始化 192 | c() 193 | print_banner() 194 | print(f"\n{Colors.BOLD}Initializing XMU Rollcall Bot...{Colors.ENDC}\n") 195 | print_separator() 196 | 197 | print(f"\n{Colors.OKCYAN}[Step 1/3]{Colors.ENDC} Checking credentials...") 198 | 199 | if os.path.exists(cookies): 200 | print(f"{Colors.OKCYAN}[Step 2/3]{Colors.ENDC} Found cached session, attempting to restore...") 201 | session_candidate = requests.Session() 202 | if l(session_candidate, cookies): 203 | profile = v(session_candidate) 204 | if profile: 205 | session = session_candidate 206 | print_login_status("Session restored successfully", True) 207 | else: 208 | print_login_status("Session expired, will re-login", False) 209 | else: 210 | print_login_status("Failed to load session", False) 211 | 212 | if not session: 213 | print(f"{Colors.OKCYAN}[Step 2/3]{Colors.ENDC} Logging in with credentials...") 214 | time.sleep(2) 215 | session = xmulogin(type=3, username=USERNAME, password=pwd) 216 | if session: 217 | s(session, cookies) 218 | print_login_status("Login successful", True) 219 | else: 220 | print_login_status("Login failed. Please check your credentials", False) 221 | time.sleep(5) 222 | exit(1) 223 | 224 | print(f"{Colors.OKCYAN}[Step 3/3]{Colors.ENDC} Fetching user profile...") 225 | profile = session.get(f"{base_url}/api/profile", headers=headers).json() 226 | name = profile["name"] 227 | print_login_status(f"Welcome, {name}", True) 228 | 229 | print(f"\n{Colors.OKGREEN}{Colors.BOLD}Initialization complete{Colors.ENDC}") 230 | print(f"\n{Colors.GRAY}Starting monitor in 3 seconds...{Colors.ENDC}") 231 | time.sleep(3) 232 | 233 | # 主循环 234 | temp_data = {'rollcalls': []} 235 | query_count = 0 236 | start_time = time.time() 237 | 238 | # 首次打印完整界面(不显示底部文字,避免下移问题) 239 | print_dashboard(name, start_time, query_count, 0, show_banner=False) 240 | 241 | # 标志位:是否已经打印过底部文字 242 | footer_initialized = False 243 | 244 | # 记录需要更新的行位置(从屏幕顶部开始计数) 245 | TIME_LINE = 10 # Current Time 所在行 246 | RUNTIME_LINE = 11 # Running Time 所在行 247 | QUERY_LINE = 12 # Query Count 所在行 248 | 249 | # 底部文字起始行 250 | FOOTER_LINE = 20 # 底部文字行 251 | 252 | def update_status_line(line_num, label, value, color): 253 | """更新指定行的状态信息,不清屏""" 254 | # 隐藏光标 255 | sys.stdout.write("\033[?25l") 256 | # 保存光标位置 257 | sys.stdout.write("\033[s") 258 | # 移动到指定行 259 | sys.stdout.write(f"\033[{line_num};0H") 260 | # 清除整行 261 | sys.stdout.write("\033[2K") 262 | # 打印新内容 263 | sys.stdout.write(f"{Colors.BOLD}{label}{Colors.ENDC} {color}{value}{Colors.ENDC}") 264 | # 恢复光标位置 265 | sys.stdout.write("\033[u") 266 | # 显示光标 267 | sys.stdout.write("\033[?25h") 268 | sys.stdout.flush() 269 | 270 | def update_footer_text(): 271 | """更新底部彩色文字,不清屏,使用固定的彩虹颜色""" 272 | text = "XMU-Rollcall-Bot @ KrsMt" 273 | colored = get_colorful_text(text, 0) # 固定使用颜色偏移0 274 | width = get_terminal_width() 275 | 276 | # 隐藏光标 277 | sys.stdout.write("\033[?25l") 278 | # 保存光标位置 279 | sys.stdout.write("\033[s") 280 | 281 | # 移动到底部文字行 282 | sys.stdout.write(f"\033[{FOOTER_LINE};0H") 283 | # 清除整行 284 | sys.stdout.write("\033[2K") 285 | 286 | # 计算居中 287 | text_len = len(text) # 纯文本长度 288 | left_padding = (width - text_len) // 2 289 | # 打印内容 290 | sys.stdout.write(' ' * left_padding + colored) 291 | 292 | # 恢复光标位置 293 | sys.stdout.write("\033[u") 294 | # 显示光标 295 | sys.stdout.write("\033[?25h") 296 | sys.stdout.flush() 297 | 298 | try: 299 | # 预分配变量,减少循环内的对象创建 300 | _last_query_time = 0 301 | 302 | while True: 303 | try: 304 | time.sleep(0.1) # 每0.1秒检查一次 305 | except KeyboardInterrupt: 306 | raise 307 | 308 | try: 309 | current_time = time.time() 310 | 311 | # 首次初始化底部文字 312 | if not footer_initialized: 313 | footer_initialized = True 314 | update_footer_text() 315 | 316 | # 每秒查询一次签到(优化:使用计数器而不是时间差计算) 317 | elapsed = int(current_time - start_time) 318 | if elapsed > _last_query_time: 319 | _last_query_time = elapsed 320 | data = session.get(rollcalls_url, headers=headers).json() 321 | query_count += 1 322 | 323 | # 只更新变化的信息,不清屏 324 | local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 325 | running_time = format_time(elapsed) 326 | 327 | update_status_line(TIME_LINE, "Current Time:", local_time, Colors.OKCYAN) 328 | update_status_line(RUNTIME_LINE, "Running Time:", running_time, Colors.OKGREEN) 329 | update_status_line(QUERY_LINE, "Query Count: ", str(query_count), Colors.WARNING) 330 | 331 | 332 | # 检查签到数据 333 | if temp_data != data: 334 | temp_data = data 335 | if len(temp_data['rollcalls']) > 0: 336 | # 有新签到时才清屏重绘 337 | c() 338 | width = get_terminal_width() 339 | print(f"\n{Colors.WARNING}{Colors.BOLD}{'!' * width}{Colors.ENDC}") 340 | print(center_text(f"{Colors.WARNING}{Colors.BOLD}NEW ROLLCALL DETECTED{Colors.ENDC}")) 341 | print(f"{Colors.WARNING}{Colors.BOLD}{'!' * width}{Colors.ENDC}\n") 342 | temp_data = a(temp_data, session) 343 | print_separator("=") 344 | print(f"\n{center_text(f'{Colors.GRAY}Press Ctrl+C to exit, continuing monitor...{Colors.ENDC}')}\n") 345 | try: 346 | time.sleep(3) 347 | except KeyboardInterrupt: 348 | # 在等待期间按 Ctrl+C,也直接跳到外层处理 349 | raise 350 | # 重新打印完整界面 351 | print_dashboard(name, start_time, query_count, 0) 352 | except KeyboardInterrupt: 353 | # 在处理过程中按 Ctrl+C,跳到外层处理 354 | raise 355 | except Exception as e: 356 | # 其他异常,显示错误并退出 357 | c() 358 | print(f"\n{center_text(f'{Colors.FAIL}{Colors.BOLD}Error occurred:{Colors.ENDC} {str(e)}')}") 359 | print(f"{center_text(f'{Colors.GRAY}Exiting...{Colors.ENDC}')}\n") 360 | exit(1) 361 | except KeyboardInterrupt: 362 | # 统一的退出处理 363 | c() 364 | print(f"\n{center_text(f'{Colors.WARNING}Shutting down gracefully...{Colors.ENDC}')}") 365 | print(f"{center_text(f'{Colors.GRAY}Total queries performed: {query_count}{Colors.ENDC}')}") 366 | print(f"{center_text(f'{Colors.GRAY}Total running time: {format_time(int(time.time() - start_time))}{Colors.ENDC}')}") 367 | print(f"\n{center_text(f'{Colors.OKGREEN}Goodbye{Colors.ENDC}')}\n") 368 | exit(0) 369 | 370 | -------------------------------------------------------------------------------- /Tronclass-URL-list/result.csv: -------------------------------------------------------------------------------- 1 | Organization Name,API URL 2 | 杭州地铁,http://10.225.29.98 3 | 空军军医-qa,https://lms-university-fmmu.tronclass.com.cn 4 | 双元教育,http://study.qtone-wr.com 5 | 西安电子科技大学,http://lms.xidian.edu.cn 6 | 孤独的阅读者,https://lms.lonelyreader.com 7 | 亞東科技大學,https://elearning.aeust.edu.tw 8 | 西安欧亚学院,https://lms.eurasia.edu 9 | ahmu,https://lms.ahmu.edu.cn 10 | 北京师范大学,https://lms.bnu.edu.cn 11 | 北京市工业技师学院,http://lms.bitc.org.cn 12 | 滁州学院,https://www.ahmooc.cn:443 13 | 天主教輔仁大學,https://elearn2.fju.edu.tw 14 | 淡江大學,https://iclass.tku.edu.tw 15 | Tamkang University,https://iclass.tku.edu.tw 16 | 中國文化大學,https://tronclass.com.tw 17 | 首钢工学院,https://lms.sgjx.com.cn 18 | 國立臺灣海洋大學,https://tronclass.ntou.edu.tw 19 | 首钢技师学院,https://lms.sgjx.com.cn 20 | 馬偕學校財團法人馬偕醫護管理專科學校,https://tronclass.mkc.edu.tw 21 | National Taiwan Ocean University,https://tronclass.ntou.edu.tw 22 | 澳門鏡湖護理學院,https://tronclass.kwnc.edu.mo 23 | 西安交通大学在线学习平台(本科生),https://class.xjtu.edu.cn 24 | National Defense Medical Center,http://210.60.124.12 25 | 上海市师资培训中心,http://wlyx.21shte.net 26 | Kiang Wu Nursing College of Macau,https://tronclass.kwnc.edu.mo 27 | KW of Macau,https://tronclass.kwnc.edu.mo 28 | Hungkuang University,https://tronclass.hk.edu.tw 29 | 弘光科技大學,https://tronclass.hk.edu.tw 30 | 弘光科技大學 Hungkuang University (line),https://tronclass.hk.edu.tw:8080 31 | "Mackay Junior College of Medicine, Nursing, and Management",https://tronclass.mkc.edu.tw 32 | 朝陽科技大學,https://tronclass.cyut.edu.tw 33 | 元培醫事科技大學,https://tronclass.ypu.edu.tw 34 | 酷課OnO線上教室,https://ono.tp.edu.tw 35 | 台灣大學進修推廣學院,https://elearn.ntuspecs.ntu.edu.tw 36 | 國立雲林科技大學,https://eclass.yuntech.edu.tw 37 | 東海大學 Tunghai University (APP登入 / APP Login),https://ilearn.thu.edu.tw 38 | 東海大學 Tunghai University (line notify),https://ilearn.thu.edu.tw 39 | 東海大學 Tunghai University (體驗網站),https://ilearn.thu.edu.tw 40 | 浙江大学,https://courses.zju.edu.cn 41 | apple-qa,https://lms-qa.tronclass.com.cn 42 | 智惠教育中心演示环境,https://uateduhub-class.zju.edu.cn 43 | 智惠教育中心,https://eduhub-class.zju.edu.cn 44 | Education Hub,https://eduhub-class.zju.edu.cn 45 | 广西英华国际职业学院,https://www.365dx.com 46 | 兰州理工大学技术工程学院,https://www.365dx.com 47 | 洛阳科技职业学院,https://www.365dx.com 48 | 北京爱因生教育投资有限责任公司,https://www.365dx.com 49 | 南京旅游职业学院,http://192.168.45.241 50 | 上海开放大学非学历培训,https://troncourse.shou.org.cn 51 | 北京市城市管理高级技术学校,http://lms.bjmac.com.cn/ 52 | 苏州大学,http://42.244.43.181 53 | 上海电力大学,http://10.166.18.62 54 | 北京市经贸高级技术学校,http://111.204.133.20:8123 55 | 安徽城市管理职业学院,http://172.168.1.27 56 | qa-arm,https://lms-arm.tronclass.com.cn 57 | wg-aliyun-stg,https://lms-stg.tronclass.com.cn 58 | AR-SEED模拟教学培训体系,https://syf.zj.zju.edu.cn 59 | 浙江研究生课程联盟,https://zjyjs.zj.zju.edu.cn 60 | 复旦大学,http://10.107.12.81 61 | 复旦大学POC,http://129.211.105.65 62 | 澳門城市大學 City University of Macau,https://tronclass.cityu.edu.mo 63 | test-local,http://192.168.11.56:5000 64 | qa-product,https://lms-qa.tronclass.com.cn 65 | qa-product-test,https://lms-qa.tronclass.com.cn 66 | AIR 演示机构,https://air-beta.tronclass.com.cn 67 | stg-dashboard,http://lms-stg.tronclass.com.cn/ 68 | qa-dashboard,http://qa-product.tronclass.com.cn 69 | project-马偕,http://qa-project.tronclass.com.cn 70 | qa-project,http://qa-project.tronclass.com.cn 71 | 實踐大學高雄校區,https://tronclass.kh.usc.edu.tw 72 | 华为openlab,http://172.16.79.194 73 | 海警学院,http://10.2.214.31 74 | AFEU,https://kjgc.tronclass.com.cn 75 | qa-product-tw,https://lms-qa.tronclass.com.cn 76 | 明道大學,https://tronclass.mdu.edu.tw 77 | 亞洲大學,https://tronclass.asia.edu.tw 78 | 阿里云智慧教学,http://47.117.138.189 79 | qa-product-org-6,https://lms-product.tronclass.com.cn 80 | 北京市工贸技师学院,http://60.247.15.152 81 | tronclass-dev,http://192.168.11.106:5000 82 | 湖南开放大学-qa,https://lms-university.tronclass.com.cn 83 | qa-sharding-master,https://lms-product.tronclass.com.cn 84 | qa-sharding-child-1,https://lms-product.tronclass.com.cn 85 | 聚碩互動課堂,https://az.tronclass.com 86 | Like It Formosa,https://az.tronclass.com 87 | CUMTEST,https://tctest.cityu.edu.mo 88 | Taiwan Digital Opportunity Center,https://tdoc.tronclass.com 89 | 学在吉大,https://jwccourses.jlu.edu.cn 90 | MS Learn,https://az.tronclass.com 91 | qa-incast-test,https://lms-qa.tronclass.com.cn 92 | 長榮大學,https://tronclass.cjcu.edu.tw 93 | Chang Jung Christian University(CJCU),https://tronclass.cjcu.edu.tw 94 | 國立虎尾科技大學,https://ulearn.nfu.edu.tw 95 | National Formosa University,https://ulearn.nfu.edu.tw 96 | qa-incast-test-ouya,https://lms-qa.tronclass.com.cn 97 | 國立東華大學,https://az.tronclass.com 98 | National Dong Hwa University,https://az.tronclass.com 99 | qa-product-org-7,https://lms-qa.tronclass.com.cn 100 | STUDIO A 晶實科技,https://az.tronclass.com 101 | 政大策略管理中心(iSVMS)/avm,https://az.tronclass.com 102 | wgtp-staging1,https://staging.tronclass.com.tw:8080 103 | Chung Shan Medical University,https://az.tronclass.com 104 | wgtp-staging2,https://staging.tronclass.com.tw:8085 105 | 國際醫療翻譯協會/imtia,https://az.tronclass.com 106 | 真理大學,https://tronclass.au.edu.tw 107 | lms_api_tests,https://api-test.tronclass.com.cn 108 | 亞太教育/asia-learning,https://az.tronclass.com 109 | 空中英語教室/StudioClassroom,https://az.tronclass.com 110 | ADP,https://az.tronclass.com 111 | CCUoversea,https://az.tronclass.com 112 | CCUoversea,https://az.tronclass.com 113 | 國立臺中教育大學/NTCU,https://az.tronclass.com 114 | National Taichung University of Education /NTCU,https://az.tronclass.com 115 | 國立臺中教育大學,https://az.tronclass.com 116 | 國泰學習網,https://cathaylife.tronclass.com 117 | 大漢技術學院/dahan,https://az.tronclass.com 118 | Dahan Institute of Technology,https://az.tronclass.com 119 | 長庚科技大學-Chang Gung of Science and Technology,https://tronclass.cgust.edu.tw 120 | Thailand,https://az.tronclass.com 121 | 翰品學院/Hanpin,https://az.tronclass.com 122 | Hanpin,https://az.tronclass.com 123 | 1999ttsl,https://az.tronclass.com 124 | 僑光科技大學/Overseas Chinese University/OCU,https://tronclass.ocu.edu.tw 125 | DSTAR,https://dstar.tronclass.com 126 | DSTAR,https://dstar.tronclass.com 127 | DSTAR_勞動部(桃竹苗分署),https://dstar-wda.tronclass.com 128 | 示範學校5,https://staging-lms.tronclass.com 129 | HIT Smart Learning,https://hit.tronclass.com 130 | 中部汽車股份有限公司,https://toyotacm.tronclass.com 131 | Central Taiwan University of Science and Technology,https://tronclass.ctust.edu.tw 132 | 泰國台商網路學院,https://ttba-online.tronclass.com 133 | INTACT Base ( 海外基地 ),https://intactchinese.csu.edu.tw 134 | Malaysia Recsam,https://my.tronclass.com 135 | 元智大學終身教育部/YZU,https://yzu.tronclass.com 136 | TronClass Workshop/TronClass 工作坊,https://ws.tronclass.com 137 | 河北农业大学,http://lms.hebau.edu.cn 138 | 勞動部勞動力發展署-淨零永續數位學習平台,https://wda-online.tronclass.com 139 | 智焄學校 / tranwisdom,https://tranwisdom.tronclass.com 140 | NetDragon,https://netdragon.tronclass.com 141 | 佳醫健康,https://excelsiormedical.tronclass.com 142 | AMC空中美語,https://amc.tronclass.com 143 | 銘傳大學 / MCU,https://mcu.tronclass.com 144 | 寀奕國際,https://taieasy.tronclass.com 145 | 媽咪樂,https://mommyhappy.tronclass.com 146 | 晴仁數位,https://sundayedu.tronclass.com 147 | AI Experience AcaDemy / AI 學習體驗學校,https://ai.tronclass.com 148 | 寰宇教育,https://elearning.globalchild.com.tw 149 | 台灣智慧解決方案協會,https://tss.tronclass.com 150 | 上海民航职业技术学院,https://lms.shcac.edu.cn 151 | FacePro College,https://facepro.tronclass.com 152 | 諾亞教育,https://know-ya.tronclass.com 153 | Kasetsart University,https://ku.tronclass.com 154 | 桓基科技,https://hgiga.tronclass.com 155 | 言果學習,https://yanguo.tronclass.com 156 | 內政部消防署(測試中),https://nfa.tronclass.com 157 | 塔木德酒店集團,https://talmud.tronclass.com 158 | 宏遠電訊,https://savecom.tronclass.com 159 | 台灣技職AI學院 (全域科技),https://twaicert.tronclass.com 160 | 台灣技職AI學院 (碁峰),https://twaigotop.tronclass.com 161 | 崇右影藝科技大學/CUFA (試用機構),https://cufa.tronclass.com 162 | 數位學習學會/AEL,https://ael.tronclass.com 163 | 臺北商業大學測試機,https://tronclass-test.ntub.edu.tw 164 | ETlab教育科技實驗室/ETlab,https://ntnuetlab.tronclass.com 165 | 國立臺北商業大學(試用環境),https://ntub.tronclass.com 166 | 精準教育保護傘,https://aiut.tronclass.com 167 | 艾科揚,https://antaira.tronclass.com 168 | AI學習平台,https://chtti.tronclass.com 169 | AIR功能體驗,https://air.tronclass.com 170 | 南亞技術學院(試用),https://nanya.tronclass.com 171 | 標竿教育基金會,https://leaderfocus.tronclass.com 172 | 逐鹿數位/metaguru,https://metaguru.tronclass.com 173 | 肯驛國際,https://canlead.tronclass.com 174 | TronClass示範學校,https://staging.tronclass.com 175 | 崇右影藝科技大學,https://tronclass.cufa.edu.tw 176 | IMVT Class,https://imvtclass.tronclass.com 177 | 亞太AI+ESG雲學院 (TAIA),https://www.taia.asia 178 | 德明財經科技大學 / Takming,https://takming.tronclass.com 179 | 師大科技系,https://ntnutahrd.tronclass.com 180 | E云课堂,https://ecourse.shanghaitech.edu.cn 181 | E云课堂-测试,http://10.15.65.155 182 | 青海建筑职业技术学院,https://qhavtc.tronclass.com.cn 183 | 消防署 UAT,https://nfa-uat.tronclass.com 184 | EDA,https://eda.tronclass.com 185 | 晉盟國際,https://chiameng.tronclass.com 186 | 南亞技術學院,https://tronclass.nanya.edu.tw 187 | ATD台灣交流會,https://ice.tronclass.com 188 | Amcmtest 澳門金管局測試環境,https://amcmtest.tronclass.com 189 | mhesi-qa,https://test-lms.mhesi-skill.org 190 | Organo,https://organo.tronclass.com 191 | Taloguru,https://www.taloguru.com 192 | 亞洲人類圖學院,https://humandesign.tronclass.com 193 | TronClass Go! AI 行動體驗站,https://go.tronclass.com 194 | 北京电子信息技师学院,https://lms.bjdzxxjsxy.cn:18888 195 | 浙大宁波理工学院,https://courses.nbt.edu.cn 196 | 北京市供销学校,http://tronclass.bjgx.com:8123 197 | 北京汽车技师学院,https://gjn.bjqjx.com:9060 198 | 北京市新媒体技师学院,http://lms.bjnmtc.com 199 | 湖北民族学院科技学院,https://www.365dx.com 200 | 北京科技高级技术学校,http://111.202.220.168:8081 201 | 北京语言大学,http://lms.blcu.edu.cn 202 | Org Hub,http://hub.tronclass.com:5000 203 | webUI自动化测试,https://lms-product.tronclass.com.cn/ 204 | 北京工商大学,http://10.0.67.72 205 | WG Business,https://wg.tronclass.com 206 | TOP-BOSS,https://top-boss.tronclass.com 207 | BlendVision,https://bv.tronclass.com 208 | 睿華國際,https://bic-service.tronclass.com 209 | 國立臺北商業大學 NTUB,https://tronclass.ntub.edu.tw 210 | 百川智能体验试用,https://bczn.tronclass.com.cn 211 | 新成教平台,https://hbkf.tronclass.com.cn 212 | JCB,https://jcb.tronclass.com.cn 213 | 富邦期貨,https://fubonfutures.tronclass.com 214 | 中南大学,https://lms.csu.edu.cn 215 | Chinese Culture University,http://tronclass.com.tw 216 | 實踐大學,https://tronclass.usc.edu.tw 217 | 富士康科技集团,http://10.201.135.201/ 218 | 靜宜大學,https://tronclass.pu.edu.tw 219 | 東吳大學,https://tronclass.scu.edu.tw 220 | 测试成绩等级,https://staging-lms.tronclass.com.tw 221 | project-数据同步,http://qa-project.tronclass.com.cn 222 | 臺北基督學院,https://cct.tronclass.com 223 | 輔仁TC測試,https://wtelearn2.fju.edu.tw 224 | 明志科技大學_管理暨設計學院,https://tronclass.mcut.edu.tw 225 | 中国科学技术大学,https://home.v.ustc.edu.cn 226 | 國立臺中科技大學,https://tc.nutc.edu.tw 227 | 國立臺中科技大學 line,https://tc.nutc.edu.tw 228 | 沈阳城市学院,http://exam.sycu.cn:30080 229 | 健行科技大學,https://az.tronclass.com 230 | 中國文化大學海外專班,https://az.tronclass.com 231 | 中国文化大学海外专班,https://az.tronclass.com 232 | 明志科技大學/Ming Chi,https://az.tronclass.com 233 | Ming Chi University of Technology,https://az.tronclass.com 234 | H3C School,https://az.tronclass.com 235 | 中臺科技大學,https://tronclass.ctust.edu.tw 236 | 國立彰化師範大學兒童英語研究所,https://az.tronclass.com 237 | 中國文化大學推廣教育部,https://pccu.tronclass.com 238 | 首頁文化,https://jyic.tronclass.com 239 | 台灣冠冕真道理財協會 / CROWN,https://crown.tronclass.com 240 | 江西财经职业学院,https://portal-elearning.jxvc.jx.cn 241 | GT College,https://thai-gt.tronclass.com 242 | 樹人醫護管理專科學校,https://szmc.tronclass.com 243 | 内江师范学院,https://course-lms.njtc.edu.cn 244 | 台塑企業技術訓練中心,https://fpg.tronclass.com 245 | 國立彰化師範大學 (試用),https://ncue.tronclass.com 246 | 中譽工業股份有限公司,https://cqstech.tronclass.com 247 | 耕莘健康管理專科學校/CTCN,https://ctcn.tronclass.com 248 | NUE,https://hjgc.tronclass.com.cn 249 | JTHK,https://hjgcf.tronclass.com.cn 250 | 中国电信,https://lms-uat.tronclass.com.cn 251 | 國立政治大學/NCCU,https://nccu.tronclass.com 252 | 財團法人醫藥品查驗中心/CDE,https://cde.tronclass.com 253 | 臺北醫學大學醫學院/TMU-COM,https://tmu-com.tronclass.com 254 | 中原大學/CYCU,https://cycu.tronclass.com 255 | 慈濟大學/TCU,https://tcu.tronclass.com 256 | 國立彰化師範大學 NCUE,https://tronclass.ncue.edu.tw 257 | 致理科技大學,https://clut.tronclass.com 258 | 國立中興大學,https://nchu.tronclass.com 259 | 重庆开放大学,https://cqkf.tronclass.com.cn 260 | 长春市机械工业学校,https://ccjx.tronclass.com.cn 261 | 核能安全委員會(DEMO),https://nusc-demo.tronclass.com 262 | 國立勤益科技大學(試用),https://ncut.tronclass.com 263 | Certiport,https://certiport.tronclass.com 264 | ESGCP,https://esgcp.tronclass.com 265 | 文大華語中心,https://mlc-ccu.tronclass.com 266 | 澳門培正中學測試環境,https://puichingtest.tronclass.com 267 | 中西創新學院,https://mmc.tronclass.com 268 | 國立勤益科技大學,https://tronclass.ncut.edu.tw 269 | 广东科学职业技术学院,http://10.30.252.77:18888 270 | 首都医科大学护理学院,http://10.0.0.5 271 | testjd,http://class.xjtu.edu.cn 272 | Gridow,https://gridow.tronclass.com 273 | 网龙大学,https://ndu.tronclass.com 274 | 广东外语外贸大学,https://lms.gdufs.edu.cn 275 | 渠道拓建,https://qdtz.tronclass.com.cn 276 | 上海交通大学,https://shjd.tronclass.com.cn 277 | 电信翼智,https://zgdx.tronclass.com.cn 278 | DD概论,https://ddgl.tronclass.com.cn 279 | 大葉大學,https://tronclass.dyu.edu.tw 280 | 唐都医院,https://tdyy.tronclass.com.cn 281 | 上海科技大学,https://shkj.tronclass.com.cn 282 | 驻马店农业工程职业学院, https://zmd.tronclass.com.cn 283 | 福建省农业职业技术学院,https://fjny.tronclass.com.cn 284 | mhesi-prod,https://mhesi-skill.org 285 | 北京市工艺美术高级技工学校,http://lms.bjgmjx.com 286 | xgj-test,https://www.365dx.com 287 | WG-test,https://ift-demo.tronclass.com 288 | stresstest_1,http://stresstest.ouchn.cn 289 | stresstest_8,https://stresstest.ouchn.cn 290 | stresstest_41,http://stresstest.ouchn.cn 291 | stresstest_130000000001,http://stresstest.ouchn.cn 292 | iStuNet,https://az.tronclass.com 293 | 樹德科技大學,https://tc.stu.edu.tw 294 | Shu-Te University,https://tc.stu.edu.tw 295 | 中南财经政法大学·私有云,https://lms.zuel.edu.cn 296 | 桂林电子科技大学,https://courses.guet.edu.cn 297 | 臺灣銀行培訓平台(測試中)/TITEST,https://titest.tronclass.com 298 | 明證管理顧問,https://minjeng.tronclass.com 299 | 台應智能公司,https://itelligensys.tronclass.com 300 | mhesi-stg,https://stg-lms.mhesi-skill.org 301 | 聯合出版集團測試環境,https://sueptest.tronclass.com 302 | 社團法人台灣寵物營養保健食品學會 / 台寵學會,https://tpnhfs.tronclass.com 303 | 佛光大學/FGU,https://fgu.tronclass.com 304 | 福建省通信产业服务有限公司,https://fjtx.tronclass.com.cn 305 | Foxlink,https://foxlink.tronclass.com 306 | 智园研究院,https://lms-uat.tronclass.com.cn 307 | 智园研究院-演示,https://lms-uat.tronclass.com.cn 308 | 北京轻工技师学院,https://lms.bjqgjsxy.cn 309 | 北京市公共交通高级技工学校,http://121.196.181.19 310 | 北京市应急管理学院,https://gjn.sgjx.com.cn:8060 311 | stg-org1,https://lms-stg.tronclass.com.cn 312 | stg自动化测试机构,https://lms-stg.tronclass.com.cn 313 | 多机构stg,https://lms-stg.tronclass.com.cn 314 | ZUNO-STG,https://lms-staging.usezuno.com 315 | wg-h,https://az.tronclass.com 316 | stg自动化测试机构,https://lms-stg.tronclass.com.cn 317 | 国际关系学院,https://gjgx.tronclass.com.cn 318 | 中国科学技术大学苏州高等研究院,https://zgkx.tronclass.com.cn 319 | 厦门理工学院,https://xmlg.tronclass.com.cn 320 | 北京市顺义区人力资源和社会保障局高级技工学校,http://course.syrbjjx.com 321 | 金禾经济研究中心,http://jcer.xjtu.edu.cn 322 | 醒吾科技大學,https://iclass.hwu.edu.tw 323 | 醒吾科技大學 (體驗網站),https://iclass.hwu.edu.tw 324 | 財團法人辭修高中,https://tsshs.tronclass.com 325 | 國家衛生研究院 / NHRI,https://nhri.tronclass.com 326 | 世新大學 / SHU,https://tronclass.shu.edu.tw 327 | 龍華科技大學,https://elearn.lhu.edu.tw 328 | 火箭军工程大学,http://192.168.1.11 329 | 江苏海洋大学,https://jshy.tronclass.com.cn 330 | 北京航空航天大学,https://hkht.tronclass.com.cn 331 | 厦门华厦学院 ,https://xmhx.tronclass.com.cn 332 | 浙江万里学院,https://hzwl.tronclass.com.cn 333 | 上海视觉艺术学院,https://shsj.tronclass.com.cn 334 | 香港都會大學測試環境,https://hkmutest.tronclass.com 335 | 工研院產業學院-雲端教室,https://collegeplus.itri.org.tw 336 | 常春藤英語,https://ivy.tronclass.com 337 | 國立宜蘭大學/NIU,https://niu.tronclass.com 338 | 國立臺南護理專科學校,https://ntin.tronclass.com 339 | 陆军工程大学(演示版),http://lms.uat.tronclass.com.cn 340 | 浙江农林大学,https://zjnl.tronclass.com.cn 341 | 浙江科技大学,https://zjkj.tronclass.com.cn 342 | 中聚智能,https://zjzn.tronclass.com.cn 343 | 南京青麦智能科技有限公司,https://njqm.tronclass.com.cn 344 | LJPY,https://ljpy.tronclass.com.cn 345 | 西交利物浦大学,https://xjlwp.tronclass.com.cn 346 | 吉首大学,https://incast.jsu.edu.cn 347 | TTKKUU,https://iclass.tku.edu.tw 348 | 台灣中小學數位學習平台 (試用),http://tw.k12.tronclass.com 349 | 國立高雄科技大學/NKUST,https://nkust.tronclass.com 350 | 高雄醫學大學/KMU,https://kmu.tronclass.com 351 | 崑山科技大學/KSU,https://ksu.tronclass.com 352 | 中医药联盟,https://zyyszlm.zj.zju.edu.cn 353 | 知策匯2.0,https://lyls.tronclass.com 354 | 黎明职业大学,https://lmxy.tronclass.com.cn 355 | 明新科技大學 MUST,https://tronclass.must.edu.tw 356 | MMOT跨領域科技管理研習班(磐安智慧財產教育基金會),https://cipfmmot.tronclass.com 357 | 厦门大学数字化教学平台,https://lnt.xmu.edu.cn 358 | TMMU,https://tmmu.tronclass.com.cn 359 | 國立台灣大學,http://lms.aca.ntu.edu.tw 360 | ZUNO,https://lms.usezuno.com 361 | 國立中山大學(試用),https://nsysu.tronclass.com 362 | 國立臺北科技大學,https://ntut.tronclass.com 363 | 國立屏東大學/NPTU,https://nptu.tronclass.com 364 | 西安盘相数字科技有限公司,https://xnpx.tronclass.com.cn 365 | 國立聯合大學/NUU,https://nuu.tronclass.com 366 | 國立中山大學,https://elearn.nsysu.edu.tw 367 | 國立空中大學_第三人生,https://3tron.nou.edu.tw 368 | 誠士資訊,https://az.tronclass.com 369 | 深圳大学,https://lms.szu.edu.cn 370 | 绍兴文理学院,https://courses.usx.edu.cn 371 | YT,https://eclass.yuntech.edu.tw 372 | 大同大學,https://ilearn.ttu.edu.tw 373 | 测试机构u,http://lms-stg.tronclass.com.cn/ 374 | 文藻外語大學 WZU,https://elearning.wzu.edu.tw 375 | 37300900,https://iclass-tst.tku.edu.tw 376 | 77308100,https://beta-tc.tp.edu.tw 377 | 52024708,https://tronclass_test.pu.edu.tw 378 | 5028 精準健康管理師聯習會,https://5028.tronclass.com 379 | 76000424,https://tronclass-test.wzu.edu.tw 380 | 365大学,https://www.365dx.com 381 | --------------------------------------------------------------------------------