写完发现自己信息已经被AI读完了,哎,跑路了,vpn,读取页面信息全部打通,后继者应该轻松很多...
南京特殊教育师范学院(NJTS)校园服务命令行工具,附一个 pi / Claude 风格的 skill 描述。
只读。 不实现任何写操作 —— 不缴费、不选课、不提交请假、不报修、不挂失。
njts schedule --http # 我这学期课表
njts card balance # 一卡通余额
njts power balance # 宿舍电费
njts leave status # 请假审批进度
| 命令 | 作用 | 状态 |
|---|---|---|
card balance / card flow |
一卡通余额、消费流水 | 可用 |
power list / power balance / power trend |
宿舍电费余额与趋势 | 可用 |
leave status |
请假审批进度(逐条的实际审批节点) | 可用 |
leave draft |
请假草稿预览 | 可用(不提交) |
schedule --http |
课表(纯 HTTP,推荐) | 可用 |
whoami / status |
统一身份认证会话、体检 | 可用 |
vpn login --via-cas / vpn status |
WebVPN 网关会话 | 可用 |
leave submit |
提交请假 | 不实现 |
power topup / card pay |
缴费、充值 | 不实现 |
| 选课、报修、挂失、阀控 | — | 不实现 |
grades / week |
成绩、周次 | 未实现(没抓到接口就不猜) |
写操作不是"还没做",是刻意不做,并且有测试钉死(test/safety.test.js 断言源码里
不出现任何写接口)。
要求 Node.js ≥ 20。零第三方运行时依赖。
为什么不是 18:代码用了
Headers.getSetCookie()(Node 19.7 起)。而每一处调用都带?.守卫 —— 在 Node 18 上不会报错,只会静默收不到任何 cookie,表现成 「莫名其妙一直未登录」。所以工具对版本做硬检查:低于 20 直接拒绝启动并说明 原因(退出码7),宁可坏得响亮。
unzip njts-cli.zip
cd njts-cli
node bin/njts.js --help想全局用就把 bin/njts.js 软链到 PATH 里:
ln -s "$PWD/bin/njts.js" /usr/local/bin/njts装进 pi:
node bin/njts.js skill install代理是自动的。 Node 的内置
fetch默认不读HTTP_PROXY/HTTPS_PROXY—— 这点和 curl 不同,很容易误判成「网络不通」。而开关NODE_USE_ENV_PROXY只在 进程启动时生效,所以在代码里改无效。工具检测到有代理变量时,会把自己重新拉起一次 并带上这个开关,不需要你手动设。想强制直连就设NJTS_NO_PROXY=1。
| 变量 | 作用 |
|---|---|
NJTS_HOME |
配置与会话目录,默认 ~/.njts-cli。放在别处(如容器里的可写目录)时用 |
NJTS_USER / NJTS_PASS |
非交互环境下的凭据(可选,见「隐私」) |
NJTS_NO_PROXY=1 |
强制直连(有代理时工具默认自动走,见下) |
njts login # 统一身份认证(RSA 模式),密码本地算,不落盘
njts vpn login --via-cas # 用 CAS 票据换 WebVPN 网关会话
njts status # 体检一卡通用的是另一套凭据(学号 + 一卡通单独密码,不是统一身份认证密码):
njts card loginnjts schedule --http # 纯 HTTP 全链,不需要浏览器(推荐)--http 全链都是普通 HTTP 请求,不启动浏览器、不依赖 JS。
所有命令的 stdout 恒为单个 JSON(人看的摘要走 stderr),所以直接
njts schedule --http | jq .data.courses 就行。退出码是契约:
2 未登录 / 3 登录失败 / 4 网络 / 5 服务端 / 6 参数 / 7 不支持。
njts card balance
njts card flow --days 30
njts power list
njts power balance --room <房间号>
njts leave status从校外到教务数据,中间隔了三层:
① 统一身份认证 (CAS) auth.njts.edu.cn —— 公网可达
② WebVPN 网关 vpn.njts.edu.cn —— 公网可达,发 TWFID 会话
③ 教务资源主机 jwxt-…-s.webvpn.njts.edu.cn:8118
门户 portal-…-s.webvpn.njts.edu.cn:8118
校外的教务只在网关后面,而"门户 → 教务"的免密登录是门户自己发起的。
完整链(src/gateway.js):
CAS 会话(公网 auth.njts.edu.cn)
→ 用 CAS 票据换网关会话 TWFID
→ 门户 portal-…-s:8118 走 CAS 票据链 → 200 主门户
→ GET /mobile/openModule.do?appName=<教务 appId>-pc
→ {"result":"1","data":"http://jwxt.njts.edu.cn/sso/Sudylogin"}
→ 把那个地址的 host 改写成网关主机(校外只有它能到)
→ 跟跳:CAS 票据 → /sso/Sudylogin → /jwglxt/ticketlogin?uid=… → 正方会话建立
→ POST /jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N253508 → 课表 JSON
两个反直觉的点,都是实测撞出来的:
- 网关的
TWFID是跨域有效的。 直接发给资源主机就放行, 不需要把 cookie 种在.webvpn.njts.edu.cn域上 —— 浏览器里那个域有 cookie, 只是因为浏览器访问过那个域名。 webvpn.njts.edu.cn根本不需要。 它的公网没有 A 记录(很多教程让你改 hosts), 但vpn.njts.edu.cn是同一台设备,https://vpn.njts.edu.cn/?redirect_uri=<目标>照样返回选路页。
课表数据的字段名全部来自接口原文,没有一个是猜的:
kcmc 课程名 / xm 教师 / xqj+ xqjmc 星期 / jcs 节次 / zcd 周次
cdmc 教室 / cdbh 教室全名 / lh 楼 / xqmc 校区
xf 学分 / zxs 总学时 / zhxs 周学时
jxb_id + jxbmc 教学班 / jxbzc 教学班组成 / kch 课程号
kclbmc 类别 / kcxz 性质 / khfsmc 考核方式 / xslxbj 标记
学年学期是从课表页自己的下拉框里读的(<option value="2026" selected="selected">),
不是写死的常量;读不到就报"没有证据"然后停手,不会发一个编出来的参数。
同样的原则贯穿整个项目:接口名只能来自页面源码,参数只能来自页面控件, 表名自己找,字段名对不上就交真实字段清单。 没有证据就不发请求。
- 会话与凭据文件权限 0600,目录 0700。
- 密码不经过命令行参数、不进 shell 历史、不进日志。
- 统一身份认证密码默认不落盘 —— CAS 会话(TGT)过期后需要重新登录。
一卡通的密码按协议需要本地加密存储(
card-credentials.json,0600)。 - 在 agent 环境里可以用 skill 提供的弹窗让用户亲自输入密码,密码不进入模型上下文。
- cookie 只报名字和域,不报值。
- CAS ticket 自动脱敏成
ticket=<脱敏>,URL 里的敏感参数同样处理。 - 诊断报告不含调试入口地址。
test/fixtures/ 里的样本来自真实响应,但姓名、学号、教师姓名已替换为占位符,
其余字段一个字节没动。详见 test/fixtures/README.md。
- 不关闭 TLS 校验,不绕过访问控制。
- 不伪造审批事由,不代替用户做任何决定。
- 只操作用户本人账号,不访问他人数据。
- 不对真实账号盲目重试登录(失败的登录尝试会被计数)。
- 只对 NJTS 有效。 网关地址、appId、接口路径都是这所学校的实测结果, 换学校要重新逆向。
- CAS 的 TGT 只有 2–3 小时,过期后需要重新登录。
njts status现在只看会话文件在不在,文件在但票据已过期时仍会报 ok —— 这是已知的假阳性,别拿它当"会话有效"的证据。(待修)- 配置目录不可写时要设
NJTS_HOME到可写目录 —— 这项还是手设的。(待修) - 成绩、周次未实现:没抓到接口,就不猜。
排错过程与每一次假阳性的复盘见 docs/NOTES.md。
npm test # node --test test/*.test.js157 个用例,零第三方依赖(test/vendor/crypto-js.min.js 只作为加密的对照基准)。
改代码时请守住几条底线,它们是这个项目被踩出来的:
- HTTP 200 不等于成功。 判定"拿到内容了"必须看正文特征 (网关选路页、登录页、"请稍候"页都可能返回 200)。
- 文本启发式只能提示,不能定生死。 已经有三次假阳性是因为 "页面里有某个词"而误判页面类型。
- 读不到就报没有证据,不要静默给默认值。 (一个静默的默认值让我差点把"0 条课"报成"课表拿到了"。)
- 样本必须诚实。 可以编样本测解析器,但必须标明是编的。
- 改完要跑真实入口(
node bin/njts.js <子命令>),不要只写个片段调同一个函数。 - 变异验证:把 bug 改回去,确认测试真的变红;抓不住的断言等于没测。
本项目是非官方的个人工具,与南京特殊教育师范学院无任何隶属关系。
它只读取使用者本人在校园系统中已有的数据,全部操作等价于使用者自己在浏览器里 点开对应页面。请自行确认你的使用方式符合学校的信息系统管理规定。 作者不对因使用本工具造成的任何后果负责。
如果你代表学校并希望删除本项目,请联系作者。