danxiapi
by CodeMzt
README.md
# danxiapi
**复旦大学校园 + 论坛 API 框架** —— 从 [DanXI-Dev/DanXi](https://github.com/DanXI-Dev/DanXi)
抽取已验证的接口,为**智能体(LLM Function Calling / MCP)**与二次开发提供
易调用、易扩展的 Python 接口。
四种形态,同一个内核:
| 形态 | 安装 | 入口 |
|---|---|---|
| Python SDK(核心) | `pip install danxiapi` | `DanXiClient` / `AsyncDanXiClient` |
| CLI | `pip install danxiapi[cli]` | `danxi` |
| HTTP 网关 | `pip install danxiapi[gateway]` | `danxi serve` |
| MCP Server | `pip install danxiapi[mcp]` | `danxi mcp` |
全要:`pip install "danxiapi[all]"`。
## 能力一览
- **统一认证**:UIS 传统登录 / Neo 新一代认证(RSA)/ 树洞 JWT,自动探测 2FA 与验证码
- **WebVPN(aTrust 2.0)**:`login_webvpn()` 走 `vpn.fudan.edu.cn` 的 OAuth2 授权码流程,
复用同一套 Neo 认证——旧的 AES-CFB 地址改写方案已随 `webvpn.fudan.edu.cn` 退役
- **树洞(FDU Hole)**:分区、树洞、楼层、搜索、收藏/订阅、消息、AI 摘要
- **教务**:学期、课表、成绩、GPA(两步 join)、考试安排、学生身份
- **校园生活**:宿舍电费、一卡通余额与流水、食堂拥挤度、图书馆拥挤度、教务公告
- **课评(DanKe)**、eHall 身份、研究生课表/成绩(优先级较低)
- **优雅退化**:图书馆 / 空教室 / WebVPN 等只在校园网内应答的能力,会在发请求前
探测可达性,不可达时抛 `FeatureUnavailable`,绝不 hang、绝不返回假数据
- **二次验证不绕过**:`my.fudan.edu.cn` 的食堂拥挤度等接口服务端要求增强认证。
SDK 会抛出 `TwoFactorRequired` 并附 `manual_login_url`——给出可手工登录的链接,
而不是假装返回空列表,也不是尝试程序化破解
## 快速开始
```python
from danxiapi import DanXiClient
with DanXiClient() as dx:
dx.login_uis() # 从 .env 读 USERID / PASSWORD
dx.login_forum() # 默认 <学号>@m.fudan.edu.cn
for course in dx.get_timetable(): # 本学期课表
print(course.name, course.teacher, course.classroom)
print(dx.get_gpa().overall_gpa)
for floor in dx.search_floors("食堂"):
print(floor.content[:80])
```
异步版本接口完全一致:
```python
async with AsyncDanXiClient() as dx:
await dx.login_uis()
courses = await dx.get_timetable()
```
> **写操作安全**:发帖 / 回复 / 点赞 / 举报全部默认 `dry_run=True`。
> 必须显式传 `dry_run=False` 才会真正发送。
## CLI
```bash
cp .env.example .env # 填入学号与密码
danxi login --forum # 登录 SSO(与树洞)
danxi whoami # 姓名 / 院系 / 专业 / 树洞状态
danxi timetable | grades | gpa | exams
danxi elec # 宿舍电费
danxi ecard balance | records # 一卡通余额 / 消费记录
danxi canteen # 食堂拥挤度(服务端要求 2FA,见下)
danxi library # 图书馆拥挤度(仅校园网)
danxi notices # 教务公告
danxi forum divisions | list | search
danxi config doctor # 主机可达性矩阵 + 凭据诊断
```
所有命令支持 `--json`,输出可直接被脚本消费;列表输出带表格渲染。
根级选项 `--verbose` / `-v` 会附上上游 URL 与响应片段,方便排查改版问题
(注意它要写在子命令之前:`danxi -v canteen`)。
### 需要二次验证的接口
`my.fudan.edu.cn` 的食堂拥挤度与电费历史在服务端开启了增强认证,仅凭账号密码
无法获取。SDK 不会猜测、也不会返回空列表冒充成功,而是直接报错并给出链接:
```
╭── TwoFactorRequired (exit 11) ─────────────────────────╮
│ 服务要求增强认证(二次验证),无法仅凭账号密码登录…… │
│ 请在浏览器中打开下面的链接,完成验证码/二次验证后重试 │
│ https://my.fudan.edu.cn/simple_list/stqk │
╰────────────────────────────────────────────────────────╯
```
这是**预期行为**:CLI 退出码 11,MCP 信封里是
`{"ok": false, "error": {"type": "TwoFactorRequired", "manual_login_url": ...}}`,
网关返回 403。在浏览器里完成一次该服务的登录后会话即生效。
### 只在校园网内可用的接口
图书馆拥挤度(`mlibrary.fudan.edu.cn`)与空教室(`10.64.130.6`)只对校园网应答。
SDK 在**发请求之前**并发探测这些主机:不可达时立即抛 `FeatureUnavailable`,
而不是让调用方卡在 socket 超时上。探测结果带缓存,`danxi config doctor` 可查看。
> 探测会兼容 `ALL_PROXY=socks://...` 环境:代理方案不合法时(未装 socksio)
> 自动改用直连重试一次,而不是把代理配置错误误报成「主机不可达」。
## HTTP 网关
```bash
danxi serve --port 8000 # http://127.0.0.1:8000/docs
```
会话是**不透明 id**(`secrets.token_urlsafe`),凭据**只存在内存**,`/logout`
与 TTL 过期即刻销毁。登录后用 `Authorization: Bearer <session_id>` 或
`HttpOnly` cookie 调用其余接口:
```bash
SID=$(curl -s localhost:8000/auth/login \
-H 'Content-Type: application/json' \
-d '{"user_id":"...","password":"..."}' | jq -r .session_id)
curl -s localhost:8000/academic/gpa -H "Authorization: Bearer $SID"
curl -s localhost:8000/campus/electricity -H "Authorization: Bearer $SID"
```
- `--read-only`:**在路由注册层面**禁掉论坛写接口(403 拒绝,而非 404),
公开演示时用
- 限流(slowapi):`/auth/login` 与论坛写接口最严;`--disable-rate-limit` 可关
- `GET /system/features`:报告当前网络探测到的可用能力(`library` / `webvpn` /
`empty_classroom`)
- 会话 TTL:空闲 30 分钟 / 绝对 8 小时,均可通过启动参数覆盖
## MCP Server
```bash
danxi mcp # stdio,凭据来自 USERID/PASSWORD 环境变量
```
挂到 Claude Code / Cursor 后,23 个工具直接可用:`login_fudan`、
`get_timetable`、`get_grades`、`get_gpa`、`get_exam_arrangement`、
`get_electricity`、`get_ecard_balance`、`get_ecard_records`、
`get_canteen_crowdedness`、`get_library_crowdedness`、`list_notices`、
`list_divisions`、`list_holes`、`get_hole`、`get_floors`、`search_floors`、
`get_ai_summary`、`list_messages`、`create_floor`、`danke_search`、
`danke_reviews` ……
约定:
- 每个工具返回 `{"ok": true, "data": ...}` 或
`{"ok": false, "error": {"type": "TwoFactorRequired", "message": ...,
"manual_login_url": ...}}`。**失败会在协议层置 `isError`**,宿主智能体不会
把「调用失败」当成「调用成功」。
- `TwoFactorRequired` / `CaptchaRequired` 携带 `manual_login_url`:需要人工在
浏览器完成认证,不要盲目重试。`get_canteen_crowdedness` 未完成 2FA 时就是
这个信封,而不是空列表。
- `FeatureUnavailable`(如 `get_library_crowdedness` 在校外)同样是错误信封:
「没有数据」和「不在校园网」对智能体是两回事。
- 写工具默认**不注册真实行为**:需要 `DX_MCP_ALLOW_WRITES=1` 启动服务端,
**并且**调用时传 `confirm=True`,缺一不可。
- `get_ai_summary` 是上游 LLM 生成的内容,可能幻觉,不可作为事实引用。
## 环境变量
| 变量 | 说明 |
|---|---|
| `USERID` / `PASSWORD` | 学号与 UIS 密码(等价于 `DX_USERID` / `DX_PASSWORD`) |
| `DX_FORUM_EMAIL` | 树洞登录邮箱,默认 `<学号>@m.fudan.edu.cn` |
| `DX_LIVE` | 设为 `1` 才跑真网络测试 |
| `DX_LIVE_ALLOW_WRITES` | 设为 `1` 才允许真网络写测试 |
| `DX_MCP_ALLOW_WRITES` | 设为 `1` 才注册 MCP 写工具 |
| `DX_GATEWAY_READ_ONLY` | 设为 `1` 启动只读网关 |
| `DX_GATEWAY_ADMIN_KEY` | 守卫 `/system/debug` 的密钥;不设则该路由不注册 |
## 设计要点
- **一次实现、同步异步双开**:所有请求逻辑(cookie / 重定向 / 登录队列 / token 刷新)
写成同步的 `httpx.BaseTransport`,同时挂到 `httpx.Client` 与 `httpx.AsyncClient`
- **手动跟随重定向**:SSO ticket 只能在中途拦截,自动重定向会丢 cookie
- **cookie epoch 守卫**:并发登录时的旧 Set-Cookie 不会覆盖新会话
(对齐 DanXi issue #701 的修复)
- **结构化数据**:全部 pydantic v2 模型,抓取型模型带 `raw` 逃生口与解析容错;
一卡通流水按**表头解析**而非按列号切片,复旦改列序时会变成解析失败而不是
静默读错金额
- **日志脱敏**:`Authorization` / cookie / 密码一律不打明文
- **核心包零依赖适配层**:`import danxiapi` 不会传递引入 typer / fastapi / mcp
(有单测在子进程里屏蔽这些模块强制保证)
## 测试
```bash
make test # 离线单测 + 集成(零网络,默认)
make test-live # 需 DX_LIVE=1 与 .env 凭据
```
三层标记,成本递增:
- `unit`(离线):纯逻辑 —— RSA 往返、aTrust OAuth2 授权码链路、cookie epoch 竞态、
重定向 walker、pydantic 解析、MCP 信封约定、可达性探测的代理降级
- `integration`(离线):`respx` 在 transport 层打桩,跑完整的
SessionEngine → transport → 解析链路
- `live`(显式开启):真网络;写操作还要 `DX_LIVE_ALLOW_WRITES=1`
离线测试在 `conftest.py` 层面 patch 掉 `httpx.Client.send`,对 `*.fduhole.com`
的非 GET 请求直接抛异常 —— **结构上不可能**从单测误发真实论坛内容。
`tests/golden/` 钉死抓取契约:复旦改版导致解析漂移时会构建失败,而不是静默返回 `[]`。
## 许可证
GPL-3.0-only,与 DanXi 上游保持一致。见 [LICENSE](LICENSE)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues