├── 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 |
--------------------------------------------------------------------------------