Metadata-Version: 2.4
Name: csniper
Version: 0.1.2
Summary: Course selection CLI
Author-email: WyattYuan <Wyatt049@outlook.com>
Requires-Python: >=3.11
Requires-Dist: httpx<0.29,>=0.28.1
Requires-Dist: python-dotenv>=1
Requires-Dist: pyyaml>=6
Requires-Dist: rich>=13
Requires-Dist: typer>=0.12
Description-Content-Type: text/markdown

# csniper

`csniper` 是一个用 Typer 实现命令行的选课工具。它把“查课、导出、解析目标、抢课”统一成 CLI 功能，抢课只是其中一个子命令。

项目约定：CLI、YAML 配置、Python 内部模型都使用规范命名；教务系统原始字段只放在 HTTP 适配层，并记录在 [命名对应.md](命名对应.md)。

## 特色功能

- **查课、解析、抢课一体化**：`search` 用于临时查课，`resolve` 用于验证 `courses.yaml` 是否能匹配到目标任务，`snipe` 才负责真正提交。
- **服务端筛选优先**：查询课程号时会尽量把课程代码、教师名传给教务接口筛选，减少全页签、全分页扫描。
- **开放时间感知**：抢课前会读取教务系统页签规则里的开放时间；如果还没开放，首次提交会安排在开放时间之后 0.05 秒，而不是提前反复撞接口。
- **克制型重试**：每门课最多尝试 5 分钟；遇到频率过高时，前 5 次保持短随机间隔，第 6 次开始指数退避，随机上限控制在 30-40 秒。
- **状态可见**：等待目标时间、进入最后 10 秒、首次提交、频率限制、未开放重试、最终结果都会在终端中提示。
- **本地配置隔离**：Cookie 写在 `.env`，目标课写在 `courses.yaml`，仓库只保留样例文件，避免把个人运行配置提交出去。

## 安装与配置

推荐普通用户用 `uv` 安装 CLI：

```powershell
uv tool install csniper
```

如果是从当前源码目录安装本地版本：

```powershell
uv tool install --force .
```

创建一个用于放配置文件的目录，并生成配置样例：

```powershell
mkdir csniper-config
cd csniper-config
csniper init
```

然后根据 `.env.example` 创建 `.env`，根据 `courses.example.yaml` 创建 `courses.yaml`。`.env` 和 `courses.yaml` 是本地运行配置，不建议提交到 git。

## `.env` 配置

登录态：

```dotenv
CSNIPER_SESSION_ID=
CSNIPER_ROUTE_COOKIE=
```

学期参数：

```dotenv
CSNIPER_AUTO_TERM=true
CSNIPER_ACADEMIC_YEAR=
CSNIPER_SEMESTER=
CSNIPER_TERM_CODE=
CSNIPER_CURRENT_ACADEMIC_YEAR=
CSNIPER_CURRENT_SEMESTER=
CSNIPER_CURRENT_TERM_CODE=
```

默认建议保持 `CSNIPER_AUTO_TERM=true`。程序会在首次查询前调用教务系统的当前选课学期接口，自动填充这 6 个值。只有在自动识别不符合预期时，才把 `CSNIPER_AUTO_TERM=false` 并手动填写这些字段。

运行参数：

```dotenv
CSNIPER_TARGET_TIME=2026-01-10 13:00:00
CSNIPER_CONCURRENCY=1
CSNIPER_INTERVAL_MIN=2
CSNIPER_INTERVAL_MAX=4
CSNIPER_REQUEST_TIMEOUT=20
CSNIPER_VERIFY_SSL=false
CSNIPER_QUERY_DELAY=1.5
```

说明：

- `CSNIPER_SESSION_ID` 对应浏览器里的 `JSESSIONID`。
- `CSNIPER_ROUTE_COOKIE` 对应浏览器里的 `route`。
- 两个 Cookie 必须来自同一个有效浏览器会话。这两个必填，可以在浏览器开发者工具的 Cookie 中找到。
- `CSNIPER_TARGET_TIME` 只影响 `snipe`，查询类命令不等待这个时间。
- `csniper doctor` 会打印实际使用的选课学期。

## 目标课程配置

编辑 `courses.yaml`：

```yaml
courses:
  - course_code: ABC1234
    teacher_name: ""
    tabs: ["all"]
    match_mode: "contains"
```

字段含义：

- `course_code`：课程代码。
- `teacher_name`：教师名过滤；空字符串表示不限制教师。
- `tabs`：规范页签名列表，或 `["all"]` 表示扫描全部页签。
- `match_mode`：课程代码匹配方式，可选 `contains` 或 `exact`。

常用写法：

```yaml
tabs: ["all"]
tabs: ["compulsory"]
tabs: ["compulsory", "elective"]
```

内置规范页签名：

| 规范页签名 | 中文显示名 |
| --- | --- |
| `compulsory` | 必修 |
| `elective` | 限选 |
| `sports` | 体育 |
| `writing_communication` | 写作与沟通 |
| `innovation_entrepreneurship` | 创新创业通选课 |
| `social_practice` | 社会实践课 |
| `competition_guidance` | 竞赛指导类课程 |
| `innovation_research` | 创新研修 |
| `innovation_experiment` | 创新实验 |
| `project_based` | 项目制课程 |
| `retake` | 重修 |
| `cross_major` | 跨专业课程体系 |
| `liberal_arts` | 文理通识 |
| `mooc` | MOOC |
| `optional` | 任选 |

如果教务系统出现新页签，先用 `csniper tabs --live --show-system-code` 查看；确认后可以写入 `courses.yaml` 的 `custom_tabs`。

查看可用页签：

```powershell
csniper tabs
```

需要查看教务系统编码时：

```powershell
csniper tabs --show-system-code
```

使用当前 Cookie 从教务系统动态发现页签：

```powershell
csniper tabs --live
```

## 常用命令

检查配置和登录态：

```powershell
csniper doctor
```

查课：

```powershell
csniper search ABC1234 --page-size 20
csniper search 水污染 --teacher 陈老师
csniper search ABC --tab cross_major
csniper search ABC1234 --all-matches
```

`search` 会尽量把课程代码、关键词和教师名传给教务接口做服务端筛选；查询类似 `ABC1234` 这种课程号时，默认在首次命中后停止扫描后续页签，以减少触发频率限制的概率。需要跨全部页签继续找同课程号时再加 `--all-matches`。默认不动态发现页签；确实需要刷新教务页签时使用 `--live-tabs`。

导出课程：

```powershell
csniper export --output courses_export.csv
```

解析 `courses.yaml` 中的目标课，但不提交：

```powershell
csniper resolve --page-size 20
```

模拟抢课流程，不提交：

```powershell
csniper snipe --dry-run --page-size 20
```

真正提交：

```powershell
csniper snipe
```

只有不带 `--dry-run` 的 `snipe` 会提交选课请求。其他命令都是只读。

`snipe` 会先解析目标课程，再等待 `CSNIPER_TARGET_TIME`。如果提交后教务系统返回“未开放 / 未到选课时间”一类提示，程序会刷新一次页签规则，读取教务系统返回的开放时间，并把下一次提交安排到开放时间之后 0.05 秒。

为了避免给教务系统造成过高压力，每门课最长尝试 5 分钟。遇到“查询请求频率过高”时，前 5 次仍使用 `CSNIPER_INTERVAL_MIN` 到 `CSNIPER_INTERVAL_MAX` 的随机间隔；第 6 次开始使用指数退避，并把随机等待上限控制在 30-40 秒之间。

## 开发

本项目使用 `uv` 管理开发环境和依赖：

```powershell
uv sync
```

编译检查：

```powershell
uv run python -m compileall csniper
```

开发环境中运行 CLI：

```powershell
uv run csniper doctor
uv run csniper search ABC1234 --page-size 20
```

构建发行包：

```powershell
uv build
uvx twine check dist/*
```

主要文件：

- `csniper/cli.py`：Typer 命令入口。
- `csniper/config.py`：读取 `.env` 和 `courses.yaml`。
- `csniper/client.py`：HTTP 适配层，集中处理教务系统原始字段。
- `csniper/tabs.py`：规范页签名和页签映射。
- `csniper/courses.py`：查课、搜索、导出、目标解析。
- `csniper/sniper.py`：等待时间、并发提交、响应判断。

## 注意事项

- 不要提交 `.env`，里面包含有效登录 Cookie。
- 不要提交 `courses.yaml`，里面是本地目标课程配置；仓库只保留 `courses.example.yaml`。
- 普通使用优先 `uv tool install csniper`；开发本项目时使用 `uv sync` 和 `uv run ...`。
- 如果浏览器能访问但 `csniper doctor` 失败，通常是 `CSNIPER_SESSION_ID` 和 `CSNIPER_ROUTE_COOKIE` 不匹配或已过期。
- 遇到“查询请求频率过高”，优先缩小 `--tab` 范围或降低分页大小；`search` 内部会使用不低于 1.5 秒的查询间隔。
