Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* text=auto eol=lf
*.bat text eol=crlf

*.jar binary
*.png binary
*.riv binary
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ captures/
.jdk*/
.codex-tmp/

# Local reverse-engineering and research caches
/apk_unpack_*/
/analysis/schedule-ui-import-fixes-*/
/analysis/unpacked-adapter-audit-*/
/analysis/ziyan-url-recovery-*/*.json
!/analysis/ziyan-url-recovery-*/verified_urls.json
/analysis/ziyan-url-recovery-*/ricraft-wakeup-schedule/

# Signing credentials and environment files
*.jks
*.keystore
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

本项目遵循语义化版本的基本约定。

## [未发布]

### 新增

- 课表空白处可直接新建课程,并自动带入所点的星期和节次。
- 默认节次时间调整为 08:00、08:50、09:50、10:40、11:30、14:30、15:20、16:20、17:10、19:00、19:50、20:40。
- 同一时段重叠时保留“非本周”课程,并优先显示本周课程;非本周课程按距离当前周的远近确定层级。
- 南航本科改用 EAMS `table0 + unitCount` 读取;新版正方增加课表 JSON 读取;强智增加常见表格变体识别。
- 金智在已验证的 `wdkb` 流程之后,依次尝试本科 `xkjglapp`、研究生 `gsapp` 和移动端 `homeapp` 课表接口。
- 教务导入允许 CAS、SSO、WebVPN 和厂商 HTTP/HTTPS 跨域跳转;特殊协议在确认后交给 Android,学校证书异常时仍需明确确认。
- 移植并改写 WakeUp 的分学校操作指引(约 30 所学校的课表页导航步骤),在导入页展示。
- 不接入智研(`ziyan`)云端解析;为其中 984 条复核候选入口并启用设备端实验解析,仍无可靠公网入口的 104 条保留为“需专用适配”。
- 目录新增辽宁经济职业技术学院、沙洲职业工学院两所可通用导入的学校。

### 质量

- 修复目录学校名拼音搜索在 Android 10 以下因 Transliterator 规则缺失导致的 lint 报错。
- 清理树维页面或学期切换时的旧响应缓存,避免误导入上一学期。
- 增加 EAMS、正方 JSON、金智移动端、强智变体、周次层级和目录状态单元测试。

## [1.0.0] - 2026-08-04

### 新增
Expand Down
9 changes: 8 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ Windows PowerShell 使用:
.\gradlew.bat lintDebug testDebugUnitTest assembleDebug
```

修改 WebView、导入流程或界面交互时,还应在已连接的模拟器或真机上执行:

```powershell
.\gradlew.bat connectedDebugAndroidTest
```

## 分支与提交

- 从最新的 `main` 创建分支。
- 建议使用 `feature/简短说明`、`fix/简短说明` 或 `docs/简短说明`。
- 建议使用 `feat/简短说明`、`fix/简短说明` 或 `docs/简短说明`。
- 每个提交只处理一个清晰的问题,避免混入无关格式化或重构。
- 不要提交 `local.properties`、签名文件、真实课程表、个人截图或构建产物。
- APK 解包、JADX 输出和临时克隆应放在项目目录之外;`analysis/` 只保留可复现脚本、脱敏的小型证据和结论。

## 代码要求

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

- 左右跟手切换周次,每周保留独立的纵向滚动位置。
- 周视图展示课程名称、教师和完整地点,支持隐藏周末与调整课程块高度。
- 手动添加、编辑和删除课程,支持单双周、课程颜色、备注和冲突检测。
- 点击课表空白处可按对应星期和节次快速添加课程;也支持编辑、删除、单双周、课程颜色、备注和冲突检测。
- 导入 `.xlsx`、JSON、CSV、HTML 和纯文本,导入前检查无效项、重复项与时间冲突。
- 支持导入覆盖后的撤销,以及包含学期和显示设置的 JSON 完整备份。
- 管理学期起始日期、总周数和当前学期。
Expand Down Expand Up @@ -67,6 +67,9 @@ app/build/outputs/apk/debug/app-debug.apk

导入前会显示课程数量、冲突、重复、无效数据,以及地点和教师字段的完整度。

- 教务目录收录 3,566 条本科、研究生和通用教务候选入口配置;其中 2,682 条可直接打开,另有 764 条已有本地解析路线、可填写学校官网确认的教务网址后试用,剩余 120 条仍需专用适配或可靠入口。“收录”不等于逐校适配或真机验证,目前只有西南石油大学经过用户真机验证。
- 目录会区分“已验证 / 通用适配 / 实验识别 / 需专用适配”。应用不接入 `ziyan` 云端解析;其中 984 条经入口复核后改走设备上的实验性通用解析,剩余 104 条只显示说明与反馈入口,不要求用户手填网址。
- 可尝试的入口会按教务类型走通用解析,并在导入预览中核对;解析不到明确的课程、星期、节次或周次时会中止,不会猜测导入。旧学校仅有 HTTP 时必须先确认明文风险。
- 旧版 `.xls` 暂不支持,请先在 Excel 中另存为 `.xlsx`。
- 应用只能导入源文件中实际存在的信息。若学校导出的文件没有教师姓名,导入预览会显示教师完整度为 `0/N`,可在课程详情中手动补充。

Expand All @@ -93,7 +96,7 @@ app/src/main/java/com/courseschedule/

## 隐私与权限

应用没有声明网络权限,也没有统计或广告 SDK。课程数据存储在本机 Room 数据库中;Android 系统备份可能按设备设置备份应用数据。权限用途和数据边界见 [隐私说明](docs/PRIVACY.md)。
应用只在用户主动使用教务网页导入时访问学校网站,没有统计或广告 SDK。课程数据存储在本机 Room 数据库中;Android 系统备份可能按设备设置备份应用数据。权限用途和数据边界见 [隐私说明](docs/PRIVACY.md)。

## 参与贡献

Expand Down
26 changes: 26 additions & 0 deletions analysis/school-adapter-v2-20260909/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# School adapter v2 work area

This directory contains only reproducible, public-endpoint audit tooling and
generated evidence summaries for the first local-parser expansion. It must not
contain credentials, cookies, authenticated timetable responses, or copied
third-party implementation code.

The runtime application continues to parse timetable data on the device. Audit
results may promote a directory entry only to `experimental`; `verified`
requires an end-to-end check by a user of that school.

Commands:

```powershell
node analysis/school-adapter-v2-20260909/upgrade-directory-v3.mjs
node analysis/school-adapter-v2-20260909/audit-public-endpoints.mjs
node analysis/school-adapter-v2-20260909/validate-directory-security.mjs
```

The security check requires one cleartext-enabled base configuration and zero
domain rules, so HTTP schools and authentication redirects never depend on a
maintained host whitelist.

The audit is deliberately non-promoting. A maintainer must add and review
school-official evidence before changing an `adapter_required` entry to
`experimental`.
77 changes: 77 additions & 0 deletions analysis/school-adapter-v2-20260909/audit-public-endpoints.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import net from "node:net";

const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, "../..");
const asset = path.join(root, "app/src/main/assets/academic_school_directory.json");
const catalog = JSON.parse(fs.readFileSync(asset, "utf8"));
const firstPhaseSourceTypes = new Set(["zf", "qz", "jz", "kingo_new", "south_soft"]);
const selected = catalog.entries.filter((entry) =>
entry.support === "adapter_required" &&
firstPhaseSourceTypes.has(entry.sourceType || "")
);

function classify(entry) {
const raw = entry.referenceUrl || entry.url || "";
if (!raw) return { reason: "missing_reference", safeUrl: "" };
let url;
try { url = new URL(raw); } catch { return { reason: "invalid_url", safeUrl: "" }; }
if (!["https:", "http:"].includes(url.protocol)) return { reason: "invalid_scheme", safeUrl: "" };
if (net.isIP(url.hostname)) return { reason: "ip_literal", safeUrl: "" };
if (url.username || url.password || url.search || url.hash) {
return { reason: "credentials_or_session_parameters", safeUrl: `${url.origin}${url.pathname}` };
}
const pathName = url.pathname || "/";
if (/vpn/i.test(url.hostname) && !/^\/(?:https?(?:-\d+)?)[/-][^/]+\//i.test(pathName)) {
return { reason: "vpn_portal_without_resource", safeUrl: `${url.origin}${pathName}` };
}
const familyPattern = {
zf: /(?:jwglxt|xskbcx|xtgl\/login|default2?\.aspx)/i,
qz: /(?:jsxsd|kbtable|xskb|login\.do)/i,
jz: /(?:jwapp|gsapp|wdkb|xkjglapp|homeapp)/i,
kingo: /(?:pageRpt|reportArea|cas\/login\.action|jsxsd)/i,
south: /(?:gmis|stulogin|student.*schedule|kb)/i
};
const family = entry.sourceType.startsWith("zf") ? "zf" :
entry.sourceType.startsWith("qz") ? "qz" :
entry.sourceType.startsWith("jz") ? "jz" :
/kingo|kg_zx|qingguo/.test(entry.sourceType) ? "kingo" : "south";
if (!familyPattern[family].test(`${url.hostname}${pathName}`)) {
return { reason: "no_family_signature", safeUrl: `${url.origin}${pathName}` };
}
return {
reason: url.protocol === "https:" ? "needs_official_source_evidence" : "http_needs_explicit_review",
safeUrl: `${url.origin}${pathName}`,
family
};
}

const rows = selected.map((entry) => ({
id: entry.id,
name: entry.name,
sourceType: entry.sourceType,
adapterId: entry.adapterId,
...classify(entry),
eligibleForPromotion: false
}));
const reasons = Object.fromEntries([...new Set(rows.map((row) => row.reason))]
.sort().map((reason) => [reason, rows.filter((row) => row.reason === reason).length]));
const report = {
catalogGeneratedAt: catalog.generatedAt || null,
directorySchemaVersion: catalog.schemaVersion,
scanned: rows.length,
promoted: 0,
policy: "Promotion requires a school-official public source, an explicit academic endpoint, safe redirect/domain review, and an observable family signature. Static upstream references alone never qualify.",
reasons,
entries: rows
};
fs.writeFileSync(path.join(here, "endpoint-audit.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
const markdown = `# 五大家族公开入口核验\n\n` +
`- 扫描条目:${rows.length}\n- 本次提升为 experimental:0\n` +
`- 结论:目录中这些 adapter_required 条目没有同时具备学校官方公开证据与可安全加载的明确入口,保持不可导入。\n\n` +
`## 原因统计\n\n` + Object.entries(reasons).map(([reason, count]) => `- ${reason}: ${count}`).join("\n") +
`\n\n完整逐条结果见 endpoint-audit.json。脚本不会下载或保存课表响应。\n`;
fs.writeFileSync(path.join(here, "endpoint-audit.md"), markdown, "utf8");
console.log(`scanned=${rows.length} promoted=0`);
Loading