bb-sync 1.10.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {bb_sync-1.10.0 → bb_sync-2.1.0}/.github/workflows/ci.yml +4 -4
  2. {bb_sync-1.10.0 → bb_sync-2.1.0}/AGENTS.md +30 -12
  3. {bb_sync-1.10.0 → bb_sync-2.1.0}/CHANGELOG.md +48 -0
  4. {bb_sync-1.10.0 → bb_sync-2.1.0}/CONTRIBUTING.md +3 -3
  5. {bb_sync-1.10.0 → bb_sync-2.1.0}/PKG-INFO +15 -7
  6. {bb_sync-1.10.0 → bb_sync-2.1.0}/README.md +14 -6
  7. {bb_sync-1.10.0 → bb_sync-2.1.0}/docs/scripting.md +7 -3
  8. {bb_sync-1.10.0 → bb_sync-2.1.0}/pyproject.toml +1 -1
  9. {bb_sync-1.10.0 → bb_sync-2.1.0}/scripts/debug_course.py +4 -7
  10. {bb_sync-1.10.0 → bb_sync-2.1.0}/scripts/debug_courses.py +4 -7
  11. {bb_sync-1.10.0 → bb_sync-2.1.0}/scripts/debug_login.py +4 -7
  12. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/__init__.py +1 -1
  13. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/blackboard/downloader.py +10 -5
  14. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/blackboard/login.py +3 -2
  15. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/blackboard/models.py +22 -0
  16. bb_sync-2.1.0/src/bb_sync/browser/__init__.py +5 -0
  17. bb_sync-2.1.0/src/bb_sync/browser/launcher.py +424 -0
  18. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/cli.py +3 -2
  19. bb_sync-2.1.0/src/bb_sync/commands/browser.py +90 -0
  20. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/course.py +4 -0
  21. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/doctor.py +1 -1
  22. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/due.py +5 -0
  23. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/run.py +5 -0
  24. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/doctor.py +52 -81
  25. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/errors.py +2 -2
  26. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/output.py +56 -9
  27. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/service.py +234 -116
  28. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/paths.py +1 -1
  29. {bb_sync-1.10.0 → bb_sync-2.1.0}/tests/conftest.py +7 -8
  30. {bb_sync-1.10.0 → bb_sync-2.1.0}/tests/test_cli_e2e.py +101 -2
  31. {bb_sync-1.10.0 → bb_sync-2.1.0}/tests/test_units.py +458 -254
  32. {bb_sync-1.10.0 → bb_sync-2.1.0}/uv.lock +2 -2
  33. bb_sync-1.10.0/src/bb_sync/browser/__init__.py +0 -5
  34. bb_sync-1.10.0/src/bb_sync/browser/steel.py +0 -511
  35. {bb_sync-1.10.0 → bb_sync-2.1.0}/.github/workflows/release.yml +0 -0
  36. {bb_sync-1.10.0 → bb_sync-2.1.0}/.gitignore +0 -0
  37. {bb_sync-1.10.0 → bb_sync-2.1.0}/.pre-commit-config.yaml +0 -0
  38. {bb_sync-1.10.0 → bb_sync-2.1.0}/LICENSE +0 -0
  39. {bb_sync-1.10.0 → bb_sync-2.1.0}/config.yaml +0 -0
  40. {bb_sync-1.10.0 → bb_sync-2.1.0}/docs/config.md +0 -0
  41. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/__main__.py +0 -0
  42. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/blackboard/__init__.py +0 -0
  43. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/blackboard/scraper.py +0 -0
  44. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/__init__.py +0 -0
  45. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/auth.py +0 -0
  46. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/commands/config.py +0 -0
  47. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/context.py +0 -0
  48. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/__init__.py +0 -0
  49. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/config.py +0 -0
  50. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/config_store.py +0 -0
  51. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/due.py +0 -0
  52. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/help.py +0 -0
  53. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/core/update.py +0 -0
  54. {bb_sync-1.10.0 → bb_sync-2.1.0}/src/bb_sync/creds.py +0 -0
  55. {bb_sync-1.10.0 → bb_sync-2.1.0}/tests/test_update_e2e.py +0 -0
@@ -76,11 +76,11 @@ jobs:
76
76
  bb-sync --version
77
77
  # 注意:doctor 的诊断输出走 stderr(stdout 只留给 --json 的结构化结果),
78
78
  # 因此必须用 2>&1 才能把体检结果落进文件供 grep 断言。
79
- # 容器里没有 Node/浏览器,doctor 会报缺项(预期,退出码非 0),只断言基础项
80
- bb-sync doctor > doctor.txt 2>&1 || true
79
+ # 容器里没有浏览器,doctor 会报缺项(预期,退出码非 0),只断言基础项。
80
+ # 网络项探的是校园站点 bb.cuhk.edu.cn(校外需要 VPN),CI 上不作要求,故跳过。
81
+ bb-sync doctor --skip-network > doctor.txt 2>&1 || true
81
82
  grep -q "✓.*Python" doctor.txt
82
83
  grep -q "✓.*git" doctor.txt
83
- grep -q "✓.*网络" doctor.txt
84
84
  # CLI 结构断言:无参数出帮助且退出码 0
85
85
  bb-sync >/dev/null
86
86
  test $? -eq 0
@@ -91,7 +91,7 @@ jobs:
91
91
  bb-sync definitely-not-a-command >/dev/null 2>&1 || rc=$?
92
92
  test "$rc" -eq 2
93
93
  # --json 的 JSON 必须落在 stdout 且可被解析(诊断走 stderr 不污染)
94
- bb-sync --json doctor 2>/dev/null | python -c "import json,sys; json.load(sys.stdin)"
94
+ bb-sync --json doctor --skip-network 2>/dev/null | python -c "import json,sys; json.load(sys.stdin)"
95
95
 
96
96
  # 分支保护要求状态检查名为 `quality`;矩阵任务改名后由这个聚合门禁继续提供。
97
97
  quality:
@@ -13,7 +13,8 @@
13
13
  PowerShell 与 zsh / bash。
14
14
  - **技术栈**:Python ≥3.11;Typer + Click(CLI)、pydantic v2(配置)、PyYAML、rich(输出)、
15
15
  keyring(凭据)、Playwright(浏览器自动化)。
16
- - **浏览器后端**:Steel——本地 Node ≥22 服务,通过 CDP 接入,Chrome profile 持久化登录态。
16
+ - **浏览器后端**:直接驱动本机已装的 Chrome / Edge——分离进程 + CDP + 持久 profile,
17
+ 不引入 Node / npm 等额外运行时(也不要为此加回任何需要联网下载的浏览器依赖)。
17
18
  - **打包**:hatchling,`src/` 布局,入口 `bb-sync = bb_sync.cli:main`。
18
19
  - **语言**:代码注释、docstring、提交信息、用户文档一律中文。安装与路径示例同时覆盖
19
20
  Windows PowerShell 和 macOS zsh / bash。
@@ -26,10 +27,10 @@
26
27
  | `src/bb_sync/context.py` | 全局 Context(`--json/--quiet/--verbose/--config`),惰性导入 |
27
28
  | `src/bb_sync/paths.py` | `BB_SYNC_HOME`(默认 `~/.bb-sync`)与配置文件查找 |
28
29
  | `src/bb_sync/creds.py` | keyring 凭据读写 |
29
- | `src/bb_sync/commands/` | 子命令:run / doctor / config / auth / course |
30
+ | `src/bb_sync/commands/` | 子命令:run / due / doctor / config / auth / course / browser |
30
31
  | `src/bb_sync/core/` | config(分层加载)、config_store(行级 YAML 编辑)、service(同步编排)、doctor、output(Console)、errors(退出码) |
31
32
  | `src/bb_sync/blackboard/` | login(ADFS SSO)、scraper(列表 / 内容抓取)、downloader、models |
32
- | `src/bb_sync/browser/steel.py` | Steel 后端定位 / 安装 / 启动、CDP 会话、持久 profile |
33
+ | `src/bb_sync/browser/launcher.py` | 本机浏览器探测 / 分离启动 / CDP 接入、持久 profile |
33
34
  | `tests/` | 全部离线:`test_units.py`(纯逻辑)+ `test_cli_e2e.py`(进程内 CLI 契约) |
34
35
  | `scripts/debug_*.py` | 手工调试脚本,会真实登录 / 访问站点;不属于测试,不要在 CI 或非必要时运行 |
35
36
 
@@ -47,9 +48,9 @@ uv run bb-sync doctor # 环境体检(触网;诊断走 stderr)
47
48
  ```
48
49
 
49
50
  以上命令在 Windows PowerShell 与 macOS zsh / bash 中相同。macOS 前置依赖可用
50
- `brew install git node uv` 安装。
51
+ `brew install git uv` 安装。
51
52
 
52
- 只有真实运行 `run` / `course list` / `doctor`(未加 `--skip-network`)才需要 Node ≥22、
53
+ 只有真实运行 `run` / `course list` / `doctor`(未加 `--skip-network`)才需要本机的
53
54
  Chrome 或 Edge 和真实凭据;测试与质量检查完全离线。
54
55
 
55
56
  ### 提交前质量门禁(改动必须全绿)
@@ -68,6 +69,9 @@ uv run pyright
68
69
  ### 1. 输出通道:stdout 只放结果,stderr 只放诊断
69
70
 
70
71
  - 所有输出走 `core.output.Console`,**禁止直接 `print`**。
72
+ - 诊断行需要强调时用 `console.line((文本, 样式), ...)` 并引用 `output.STYLE_*` 语义色板,
73
+ 不要在调用点新造颜色名;**不要打开 Rich markup**——诊断里到处是 `[new]`、`[作业]`
74
+ 这类方括号文本,会被当成标签。着色在非 TTY、`NO_COLOR`、`--json` 下自动关闭。
71
75
  - `--json` 时 stdout 有且仅有一个 JSON 文档;出错时 stdout 保持为空,错误走 stderr。
72
76
  - 新增命令或字段时,同步更新 `docs/scripting.md` 的 JSON 结构表与对应测试。
73
77
 
@@ -90,17 +94,29 @@ CLI 参数 > `BB_SYNC_<KEY>` 环境变量 > config.yaml > 内置默认。
90
94
  `config set/unset` 通过 `core/config_store.py` 做**行级编辑**,必须保留注释与未知键;
91
95
  不要改成整份 YAML 重写。路径常量在 import 时快照,测试靠 monkeypatch 覆盖。
92
96
 
93
- ### 5. Steel 后端锚定快照
97
+ ### 5. 浏览器后端:分离进程 + 自有 profile
94
98
 
95
- `browser/steel.py` 的 `STEEL_REF` 固定到验证过的 commit,不追随上游 `main`;
96
- 升级方式 = 改 `STEEL_REF` 并在 `CHANGELOG.md` 记录。
97
- Steel 安装目录只能是 `BB_SYNC_HOME/.steel`,任何情况下不读写用户家目录的 `~/.steel`。
99
+ 浏览器就是用户机器上的 Chrome / Edge,由 `browser/launcher.py` 以
100
+ `--remote-debugging-port=0` 拉起,Playwright 只当 CDP 客户端。四条不能破的约束:
101
+
102
+ - **跑完自动关**:默认命令结束(成功或失败)就关掉浏览器,不留后台进程。
103
+ `browser_session` 的 finally 负责收尾,`--keep-open` 才保留实例供手动过 MFA
104
+ 或连续多次运行——不要在命令里留下没人管的浏览器。
105
+ - **分离**:进程独立于 CLI 存活(Windows 用 `CREATE_NO_WINDOW|CREATE_NEW_PROCESS_GROUP`,
106
+ POSIX 用 `start_new_session`),命令跑到一半终端被关掉也不会把它带走,
107
+ `--keep-open` 保留下来的实例下次运行直接复用。
108
+ - **端口自发现**:只认浏览器自己写下的 `DevToolsActivePort`,不要改成固定端口或自建注册表。
109
+ - **profile 隔离**:`--user-data-dir` 只能是 `BB_SYNC_HOME/.browser-profile/<chrome|edge>`,
110
+ 任何情况下不读不写用户日常使用的浏览器 profile。
111
+ - **关闭方式**:只能走 CDP 的 `Browser.close`(`stop_browser`),不要按 PID 杀进程——
112
+ 记录下来的 PID 可能已被系统回收。
98
113
 
99
114
  ## 测试要求
100
115
 
101
116
  - 默认测试套件必须离线:不登录、不下载、不起浏览器、不碰真实钥匙串。
102
- `tests/conftest.py` 自动隔离 `BB_SYNC_HOME`、CWD、keyring 与 Steel 路径——
103
- 新测试直接依赖既有 fixture(`cli`、`config_file` 等),不要连接真实环境。
117
+ `tests/conftest.py` 自动隔离 `BB_SYNC_HOME`、CWD、keyring 与浏览器 profile 路径——
118
+ 新测试直接依赖既有 fixture(`cli`、`config_file` 等),不要连接真实环境,
119
+ 更不要在测试里真的拉起浏览器进程。
104
120
  - 新行为要带测试:纯逻辑进 `tests/test_units.py`,CLI 契约 / 退出码 / 输出分流进 `tests/test_cli_e2e.py`。
105
121
  - 真实站点逻辑(登录、抓取、下载)无法离线覆盖时,在提交信息或 PR 里写清可重复的手工验证步骤。
106
122
 
@@ -126,5 +142,7 @@ Steel 安装目录只能是 `BB_SYNC_HOME/.steel`,任何情况下不读写用
126
142
  - GitHub Actions 的 run 默认 `set -e`,断言非零退出码需用 `|| true` 兜住(见 `ci.yml` 注释)。
127
143
  - Windows 与 macOS 是主要目标平台,CI 同时跑 Ubuntu、macOS 与 Windows;路径、后台进程和
128
144
  浏览器探测相关改动两个平台都要考虑。
129
- - 首次准备 Steel 需从 GitHub / npm 拉依赖,国内网络慢时可临时用代理或镜像,不要写永久全局配置。
145
+ - 浏览器后端不需要任何联网安装步骤;如果哪天又引入需要下载的组件,先想清楚国内网络下的代价。
146
+ - 关闭后台浏览器只能走 CDP(`browser stop`);直接 kill 浏览器进程会留下失效的
147
+ `DevToolsActivePort`,虽然下次启动会清掉,但 profile 可能没落盘。
130
148
  - 下载目录默认 `~/courses`,课程资料有版权;禁止提交或外传 `courses/`、`.workbuddy/` 内容。
@@ -2,6 +2,54 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [2.1.0] — 2026-10-31
6
+
7
+ ### 新增
8
+
9
+ - `bb-sync run` 在「汇总」之后新增「新下载明细」,逐行列出本轮真正新落盘的文件
10
+ 及其课程(相对课程目录的路径,作业带 `hwN/`),例如
11
+ `[new] CSC5010 | assignments/hw2/report.pdf`;没有新文件时不打印。`--json run`
12
+ 同步增加 `files` 数组(`course` / `category` / `name` / `path`,其中 `path` 为
13
+ 绝对路径),脚本可以直接取用。
14
+ - 诊断输出改用语义色板:章节标题加粗、课程代码与 `[due]` / `[new]` 前缀青色、
15
+ 新下载绿色、dry-run 与待办黄色、逾期红色、测验 / 考试徽标紫色、关键数字加粗,
16
+ 正文从淡灰色恢复为终端默认前景色。颜色只在 `core/output.py` 定义一处;新增
17
+ `Console.line((文本, 样式), ...)` 按片段上色,不启用 Rich markup——诊断里的
18
+ 方括号文本(`[new]`、`[作业]`)不会被误判成标签。
19
+ - 着色随终端能力自动开关:输出被重定向、终端不支持颜色、设置 `NO_COLOR`
20
+ (任意非空值)或 `--json` 时都不输出转义序列,stdout 的 JSON 保持纯净。
21
+
22
+ ## [2.0.0] — 2026-10-14
23
+
24
+ ### 变更(破坏性)
25
+
26
+ - **移除 Steel 浏览器后端,改用本机已装的 Chrome / Edge**(没有 Chrome 时自动回退
27
+ Edge),通过 CDP 驱动。不再需要 Node.js / npm,首次运行也不再从 GitHub 下载
28
+ 约 90 MB 源码再跑 `npm install`——此前首次准备要几分钟、安装目录约 700 MB,
29
+ 现在这些全部省掉,且不再依赖 github.com 可达。
30
+ - 浏览器以**分离进程**运行——独立于 CLI 存活,命令跑到一半关掉终端也不会被带走——
31
+ 但**跑完会自动关闭**,成功或失败都不留后台进程。保留状态下的冷启动 0.3 秒、
32
+ 复用 0.01 秒(本机实测)。
33
+ - **登录态位置变更**:profile 改存 `~/.bb-sync/.browser-profile/<chrome|edge>`,
34
+ 登录态仍跨次运行保留;旧目录 `steel-chrome` 不再使用,升级后需要重新登录一次。
35
+ - `bb-sync doctor` 去掉 Node.js / npm 两项(现在只依赖浏览器),网络项改为探测
36
+ Blackboard 站点本身——原来探 github.com 只是为了下载 Steel。
37
+ - 旧版 `~/.bb-sync/.steel` 目录如仍存在,`bb-sync doctor` 会提示手动删除以释放空间。
38
+
39
+ ### 新增
40
+
41
+ - `--keep-open`(`run` / `due` / `course list`):跑完保留后台浏览器,供手动过 MFA、
42
+ 连续多次运行或调试;配套 `bb-sync browser status` 查看状态、`bb-sync browser stop`
43
+ 关闭(登录态保留在 profile,下次同步会自动重启)。
44
+ - 抹掉 `navigator.webdriver`:浏览器一旦开启远程调试端口,这个值就是 true,
45
+ 站点常据此判定「被自动化控制」;现在通过 init script 处理,与旧后端的反检测
46
+ 能力保持对齐。
47
+
48
+ ### 测试
49
+
50
+ - 删除 Steel 部署 / 回滚 / 锚点相关用例,新增浏览器探测、启动参数、端口文件解析、
51
+ 实例复用与显示模式切换、CDP 关闭等用例;`doctor` 的检查项回归防线同步更新。
52
+
5
53
  ## [1.10.0] — 2026-10-14
6
54
 
7
55
  ### 新增
@@ -9,9 +9,9 @@
9
9
  - Python 3.11 或更高版本;
10
10
  - [uv](https://docs.astral.sh/uv/);
11
11
  - Git;
12
- - Node.js 22+ 与 npm
12
+ - Chrome 或 Edge(只有真实跑 `run` / `course list` 时才用到)
13
13
 
14
- macOS 可用 `brew install git node uv`;Windows 可按 README 用 `winget` 安装。
14
+ macOS 可用 `brew install git uv`;Windows 可按 README 用 `winget` 安装。
15
15
 
16
16
  初始化并安装开发依赖:
17
17
 
@@ -54,7 +54,7 @@ uv run pyright
54
54
  - pytest;
55
55
  - 干净容器中的安装冒烟测试。
56
56
 
57
- 启动 Steel、登录 Blackboard 和真实下载不属于默认离线测试范围。修改相关逻辑时,请在本地补充可重复的手工验证步骤。
57
+ 启动浏览器、登录 Blackboard 和真实下载不属于默认离线测试范围。修改相关逻辑时,请在本地补充可重复的手工验证步骤。
58
58
 
59
59
  ## 提交习惯
60
60
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bb-sync
3
- Version: 1.10.0
3
+ Version: 2.1.0
4
4
  Summary: 港中深(CUHK-Shenzhen)Blackboard 课件下载与课程资源自动同步:登录 ADFS SSO,增量下载课件/作业/指导、公告与待办
5
5
  Project-URL: Homepage, https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
6
6
  Project-URL: Repository, https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
@@ -135,7 +135,7 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
135
135
 
136
136
  ## 安装
137
137
 
138
- 需要 Git、Node.js 22+(含 npm)、uv 和 Chrome 或 Edge。bb-sync 会自动探测常见安装位置。
138
+ 需要 uv 和 Chrome 或 Edge。bb-sync 会自动探测常见安装位置,Windows 自带的 Edge 直接可用。
139
139
 
140
140
  ### Windows
141
141
 
@@ -143,10 +143,11 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
143
143
 
144
144
  ```powershell
145
145
  winget install --id Git.Git -e
146
- winget install --id OpenJS.NodeJS.LTS -e
147
146
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
148
147
  ```
149
148
 
149
+ (Git 只在「从 GitHub 安装」时需要,走 PyPI 安装可以跳过。)
150
+
150
151
  Windows 自带的 Edge 即可使用;也可以自行安装 Google Chrome。
151
152
 
152
153
  ### macOS
@@ -154,7 +155,7 @@ Windows 自带的 Edge 即可使用;也可以自行安装 Google Chrome。
154
155
  推荐先安装 [Homebrew](https://brew.sh/),再执行:
155
156
 
156
157
  ```bash
157
- brew install git node uv
158
+ brew install git uv
158
159
  brew install --cask google-chrome
159
160
  ```
160
161
 
@@ -175,7 +176,7 @@ bb-sync doctor
175
176
  uv tool install git+https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
176
177
  ```
177
178
 
178
- `bb-sync doctor` 用于检查环境;刚安装时凭据或浏览器后端显示“未配置”是正常的。如果提示找不到 `bb-sync`,执行 `uv tool update-shell` 后重开终端。校外使用 Blackboard 时,可能需要先连接学校 VPN。
179
+ `bb-sync doctor` 用于检查环境;刚安装时凭据显示“未配置”、浏览器后端显示“未运行”都是正常的。如果提示找不到 `bb-sync`,执行 `uv tool update-shell` 后重开终端。校外使用 Blackboard 时,可能需要先连接学校 VPN。
179
180
 
180
181
  ## 首次使用
181
182
 
@@ -193,12 +194,16 @@ bb-sync auth login
193
194
  bb-sync run --headed
194
195
  ```
195
196
 
196
- 首次运行会自动准备浏览器后端,可能需要几分钟。保持网络连接,不要提前关闭终端。之后同步直接运行:
197
+ 这条命令会用你机器上的 Chrome(没有则用 Edge)打开一个窗口,登录一次即可。之后同步直接运行:
197
198
 
198
199
  ```bash
199
200
  bb-sync run
200
201
  ```
201
202
 
203
+ 浏览器默认在**后台无窗口**运行,**跑完自动关闭**,不会在你机器上留下进程。登录态存在 `~/.bb-sync/.browser-profile/` 下、跨次运行保留,所以不必每次登录。
204
+
205
+ 需要把窗口留在屏幕上时(手动过 MFA、或想连续跑几条命令)加 `--keep-open`,之后再 `bb-sync browser stop` 关闭。
206
+
202
207
  ## 文件位置
203
208
 
204
209
  默认下载到用户主目录下的 `courses`:
@@ -230,12 +235,15 @@ Windows 也可以使用 `bb-sync config set root "D:/University/courses"`。更
230
235
  | `bb-sync run --headed` | 显示浏览器,适合首次登录或 MFA |
231
236
  | `bb-sync run --course CSC5010` | 只同步指定课程,可重复使用 |
232
237
  | `bb-sync run --dry-run` | 预览下载和待办,不写文件 |
238
+ | `bb-sync run --keep-open` | 跑完保留后台浏览器(手动过 MFA / 连着跑多条命令) |
233
239
  | `bb-sync due` | 扫描全部课程主页的 Due / To Do 与公告测验、考试,生成 `due.md` |
234
240
  | `bb-sync due --course CSC5010` | 只整理指定课程的待办 |
235
241
  | `bb-sync course list` | 列出账号下的课程 |
236
242
  | `bb-sync auth login` / `logout` / `status` | 录入、删除、查看已保存的凭据 |
237
243
  | `bb-sync config show` | 显示配置文件内容 |
238
244
  | `bb-sync config set <键> <值>` | 修改配置;另有 `get` / `unset` / `edit` / `path` / `init` |
245
+ | `bb-sync browser status` | 查看后台浏览器是否在运行(`--keep-open` 留下的实例) |
246
+ | `bb-sync browser stop` | 关闭后台浏览器(登录态保留) |
239
247
  | `bb-sync doctor` | 检查环境是否就绪 |
240
248
  | `bb-sync --help` | 查看完整帮助 |
241
249
 
@@ -243,7 +251,7 @@ Windows 也可以使用 `bb-sync config set root "D:/University/courses"`。更
243
251
 
244
252
  - **`bb-sync` 找不到**:重开终端,或执行 `uv tool update-shell`。
245
253
  - **环境检查不通过**:按 `bb-sync doctor` 的提示安装或配置缺失项。
246
- - **第一次运行很慢**:首次需要准备浏览器后端,耐心等待。
254
+ - **想留住浏览器窗口**:默认跑完会自动关;加 `--keep-open` 才会保留,之后用 `bb-sync browser status` 看状态、`bb-sync browser stop` 关闭(不影响登录态)。
247
255
  - **登录失败或需要 MFA**:运行 `bb-sync auth status` 查看凭据,再用 `bb-sync run --headed` 重试。
248
256
  - **更换密码**:重新运行 `bb-sync auth login`。
249
257
 
@@ -84,7 +84,7 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
84
84
 
85
85
  ## 安装
86
86
 
87
- 需要 Git、Node.js 22+(含 npm)、uv 和 Chrome 或 Edge。bb-sync 会自动探测常见安装位置。
87
+ 需要 uv 和 Chrome 或 Edge。bb-sync 会自动探测常见安装位置,Windows 自带的 Edge 直接可用。
88
88
 
89
89
  ### Windows
90
90
 
@@ -92,10 +92,11 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
92
92
 
93
93
  ```powershell
94
94
  winget install --id Git.Git -e
95
- winget install --id OpenJS.NodeJS.LTS -e
96
95
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
97
96
  ```
98
97
 
98
+ (Git 只在「从 GitHub 安装」时需要,走 PyPI 安装可以跳过。)
99
+
99
100
  Windows 自带的 Edge 即可使用;也可以自行安装 Google Chrome。
100
101
 
101
102
  ### macOS
@@ -103,7 +104,7 @@ Windows 自带的 Edge 即可使用;也可以自行安装 Google Chrome。
103
104
  推荐先安装 [Homebrew](https://brew.sh/),再执行:
104
105
 
105
106
  ```bash
106
- brew install git node uv
107
+ brew install git uv
107
108
  brew install --cask google-chrome
108
109
  ```
109
110
 
@@ -124,7 +125,7 @@ bb-sync doctor
124
125
  uv tool install git+https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
125
126
  ```
126
127
 
127
- `bb-sync doctor` 用于检查环境;刚安装时凭据或浏览器后端显示“未配置”是正常的。如果提示找不到 `bb-sync`,执行 `uv tool update-shell` 后重开终端。校外使用 Blackboard 时,可能需要先连接学校 VPN。
128
+ `bb-sync doctor` 用于检查环境;刚安装时凭据显示“未配置”、浏览器后端显示“未运行”都是正常的。如果提示找不到 `bb-sync`,执行 `uv tool update-shell` 后重开终端。校外使用 Blackboard 时,可能需要先连接学校 VPN。
128
129
 
129
130
  ## 首次使用
130
131
 
@@ -142,12 +143,16 @@ bb-sync auth login
142
143
  bb-sync run --headed
143
144
  ```
144
145
 
145
- 首次运行会自动准备浏览器后端,可能需要几分钟。保持网络连接,不要提前关闭终端。之后同步直接运行:
146
+ 这条命令会用你机器上的 Chrome(没有则用 Edge)打开一个窗口,登录一次即可。之后同步直接运行:
146
147
 
147
148
  ```bash
148
149
  bb-sync run
149
150
  ```
150
151
 
152
+ 浏览器默认在**后台无窗口**运行,**跑完自动关闭**,不会在你机器上留下进程。登录态存在 `~/.bb-sync/.browser-profile/` 下、跨次运行保留,所以不必每次登录。
153
+
154
+ 需要把窗口留在屏幕上时(手动过 MFA、或想连续跑几条命令)加 `--keep-open`,之后再 `bb-sync browser stop` 关闭。
155
+
151
156
  ## 文件位置
152
157
 
153
158
  默认下载到用户主目录下的 `courses`:
@@ -179,12 +184,15 @@ Windows 也可以使用 `bb-sync config set root "D:/University/courses"`。更
179
184
  | `bb-sync run --headed` | 显示浏览器,适合首次登录或 MFA |
180
185
  | `bb-sync run --course CSC5010` | 只同步指定课程,可重复使用 |
181
186
  | `bb-sync run --dry-run` | 预览下载和待办,不写文件 |
187
+ | `bb-sync run --keep-open` | 跑完保留后台浏览器(手动过 MFA / 连着跑多条命令) |
182
188
  | `bb-sync due` | 扫描全部课程主页的 Due / To Do 与公告测验、考试,生成 `due.md` |
183
189
  | `bb-sync due --course CSC5010` | 只整理指定课程的待办 |
184
190
  | `bb-sync course list` | 列出账号下的课程 |
185
191
  | `bb-sync auth login` / `logout` / `status` | 录入、删除、查看已保存的凭据 |
186
192
  | `bb-sync config show` | 显示配置文件内容 |
187
193
  | `bb-sync config set <键> <值>` | 修改配置;另有 `get` / `unset` / `edit` / `path` / `init` |
194
+ | `bb-sync browser status` | 查看后台浏览器是否在运行(`--keep-open` 留下的实例) |
195
+ | `bb-sync browser stop` | 关闭后台浏览器(登录态保留) |
188
196
  | `bb-sync doctor` | 检查环境是否就绪 |
189
197
  | `bb-sync --help` | 查看完整帮助 |
190
198
 
@@ -192,7 +200,7 @@ Windows 也可以使用 `bb-sync config set root "D:/University/courses"`。更
192
200
 
193
201
  - **`bb-sync` 找不到**:重开终端,或执行 `uv tool update-shell`。
194
202
  - **环境检查不通过**:按 `bb-sync doctor` 的提示安装或配置缺失项。
195
- - **第一次运行很慢**:首次需要准备浏览器后端,耐心等待。
203
+ - **想留住浏览器窗口**:默认跑完会自动关;加 `--keep-open` 才会保留,之后用 `bb-sync browser status` 看状态、`bb-sync browser stop` 关闭(不影响登录态)。
196
204
  - **登录失败或需要 MFA**:运行 `bb-sync auth status` 查看凭据,再用 `bb-sync run --headed` 重试。
197
205
  - **更换密码**:重新运行 `bb-sync auth login`。
198
206
 
@@ -12,6 +12,10 @@ bb-sync 按 Unix CLI 惯例严格分流输出:**结果走 stdout,诊断走 s
12
12
 
13
13
  > 自动更新检查的提示也走 stderr,不会污染 stdout;`--quiet` 下会跳过检查。
14
14
 
15
+ 彩色只在交互式终端里启用:stdout 被重定向、终端不支持颜色、设置了 `NO_COLOR`
16
+ (任意非空值)或加了 `--json` 时都会自动关闭。所以 `bb-sync run 2>&1 | tee log.txt`
17
+ 这类管道里拿到的是纯文本,stdout 的 JSON 更是永远不含转义序列。
18
+
15
19
  ## `--json`:机器可读输出
16
20
 
17
21
  `--json` 是**全局选项**,位置在子命令之前:
@@ -35,7 +39,7 @@ bb-sync --json doctor | jq '.ok'
35
39
  | 命令 | stdout 的 JSON |
36
40
  | --- | --- |
37
41
  | `course list` | 数组:`[{"id","code","title","folder"}, ...]` |
38
- | `run` | 对象:`{"downloaded","exists","would_download","empty","failed","courses"}` |
42
+ | `run` | 对象:`{"downloaded","exists","would_download","empty","failed","courses","files"}`;`files` 是本次**真正新落盘**的文件(dry-run 时为空),元素为 `{"course","category","name","path"}`——`name` 相对课程目录、`path` 为绝对路径 |
39
43
  | `due` | 对象:`{"path","total","failed","announcement_failed","courses","items"}`;`items` 含课程、标题、类型(`task` / `quiz` / `exam`)与 ISO 日期 |
40
44
  | `doctor` | 对象:`{"ok": bool, "checks": [{"label","ok","detail","hint","required"}]}` |
41
45
  | `doctor --list` | 对象:`{"checks": ["python","git", ...]}` |
@@ -72,7 +76,7 @@ fi
72
76
  | 4 | 配置文件问题 |
73
77
  | 5 | 网络 / 代理 / 下载失败 |
74
78
  | 6 | 目标不存在(课程、配置键等) |
75
- | 7 | 环境依赖缺失(Node / 浏览器 / Steel) |
79
+ | 7 | 环境依赖缺失(未检测到浏览器、浏览器启动失败等) |
76
80
  | 8 | 用户中断 |
77
81
 
78
82
  > 未捕获的意外异常统一归为 1,并会把堆栈打到 stderr。
@@ -85,7 +89,7 @@ case $? in
85
89
  0) echo "同步完成" ;;
86
90
  3) echo "需要先 bb-sync auth login" >&2 ;;
87
91
  5) echo "网络/代理异常,检查 Clash 是否开启" >&2 ;;
88
- 7) echo "缺 Node/浏览器,先跑 bb-sync doctor" >&2 ;;
92
+ 7) echo "缺浏览器,先跑 bb-sync doctor" >&2 ;;
89
93
  *) echo "其它错误" >&2 ;;
90
94
  esac
91
95
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bb-sync"
7
- version = "1.10.0"
7
+ version = "2.1.0"
8
8
  description = "港中深(CUHK-Shenzhen)Blackboard 课件下载与课程资源自动同步:登录 ADFS SSO,增量下载课件/作业/指导、公告与待办"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -14,7 +14,7 @@ from playwright.sync_api import sync_playwright
14
14
 
15
15
  from bb_sync.blackboard.login import ensure_login
16
16
  from bb_sync.blackboard.models import BASE
17
- from bb_sync.browser import steel
17
+ from bb_sync.browser import launcher
18
18
  from bb_sync.core.output import Console
19
19
  from bb_sync.paths import BB_SYNC_HOME
20
20
 
@@ -23,10 +23,9 @@ COURSE_ID = sys.argv[1] if len(sys.argv) > 1 else "_18482_1" # CSC5010
23
23
 
24
24
 
25
25
  def main() -> None:
26
- steel.ensure_server(console)
27
- session = steel.create_session(console=console)
26
+ session = launcher.ensure_browser(console)
28
27
  with sync_playwright() as p:
29
- browser, ctx, page = steel.connect(p, session)
28
+ browser, ctx, page = launcher.connect(p, session)
30
29
  ensure_login(page, console)
31
30
 
32
31
  for label, url in [
@@ -72,9 +71,7 @@ def main() -> None:
72
71
  for link in interesting[:20]:
73
72
  console.log(f" {link[:150]}")
74
73
 
75
- ctx.close()
76
- browser.close()
77
- steel.release_session(session, console)
74
+ # 调试脚本故意不关浏览器(方便接着看现场);用完 bb-sync browser stop 关闭
78
75
 
79
76
 
80
77
  if __name__ == "__main__":
@@ -13,7 +13,7 @@ from playwright.sync_api import sync_playwright
13
13
 
14
14
  from bb_sync.blackboard.login import ensure_login
15
15
  from bb_sync.blackboard.models import BASE
16
- from bb_sync.browser import steel
16
+ from bb_sync.browser import launcher
17
17
  from bb_sync.core.output import Console
18
18
  from bb_sync.paths import BB_SYNC_HOME
19
19
 
@@ -22,10 +22,9 @@ PORTAL = f"{BASE}/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1"
22
22
 
23
23
 
24
24
  def main() -> None:
25
- steel.ensure_server(console)
26
- session = steel.create_session(console=console)
25
+ session = launcher.ensure_browser(console)
27
26
  with sync_playwright() as p:
28
- browser, ctx, page = steel.connect(p, session)
27
+ browser, ctx, page = launcher.connect(p, session)
29
28
  ensure_login(page, console)
30
29
  console.success("已登录")
31
30
 
@@ -69,9 +68,7 @@ def main() -> None:
69
68
  for link in links:
70
69
  console.log(f" {link[:140]}")
71
70
 
72
- ctx.close()
73
- browser.close()
74
- steel.release_session(session, console)
71
+ # 调试脚本故意不关浏览器(方便接着看现场);用完 bb-sync browser stop 关闭
75
72
 
76
73
 
77
74
  if __name__ == "__main__":
@@ -12,7 +12,7 @@ import re
12
12
  from playwright.sync_api import sync_playwright
13
13
 
14
14
  from bb_sync.blackboard.models import LOGIN_URL
15
- from bb_sync.browser import steel
15
+ from bb_sync.browser import launcher
16
16
  from bb_sync.core.output import Console
17
17
  from bb_sync.creds import load_credentials
18
18
  from bb_sync.paths import BB_SYNC_HOME
@@ -45,10 +45,9 @@ def dump(page, tag: str) -> None:
45
45
 
46
46
 
47
47
  def main() -> None:
48
- steel.ensure_server(console)
49
- session = steel.create_session(console=console)
48
+ session = launcher.ensure_browser(console)
50
49
  with sync_playwright() as p:
51
- browser, ctx, page = steel.connect(p, session)
50
+ browser, ctx, page = launcher.connect(p, session)
52
51
  page.goto(LOGIN_URL, wait_until="domcontentloaded")
53
52
  dump(page, "01_bb_login")
54
53
 
@@ -70,9 +69,7 @@ def main() -> None:
70
69
 
71
70
  page.wait_for_timeout(5000)
72
71
  dump(page, "07_final")
73
- ctx.close()
74
- browser.close()
75
- steel.release_session(session, console)
72
+ # 调试脚本故意不关浏览器(方便接着看现场);用完 bb-sync browser stop 关闭
76
73
 
77
74
 
78
75
  if __name__ == "__main__":
@@ -5,7 +5,7 @@
5
5
  - :mod:`bb_sync.cli` Typer 命令行入口
6
6
  - :mod:`bb_sync.core` 配置、输出、异常等基础设施
7
7
  - :mod:`bb_sync.blackboard` Blackboard 站点交互(登录 / 抓取 / 下载)
8
- - :mod:`bb_sync.browser` Steel 浏览器后端
8
+ - :mod:`bb_sync.browser` 本机浏览器后端(分离进程 + CDP)
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -192,8 +192,11 @@ def _content_length(headers) -> int | None:
192
192
  return None
193
193
 
194
194
 
195
- def _category_dir(course_dir: Path, item: FileItem) -> Path:
196
- """条目落盘目录;作业按 hwN 归档,对齐已有目录习惯。"""
195
+ def category_dir(course_dir: Path, item: FileItem) -> Path:
196
+ """条目落盘目录;作业按 hwN 归档,对齐已有目录习惯。
197
+
198
+ 对外的唯一含义是「这个条目会写到哪里」,汇总明细与真实写入共用它。
199
+ """
197
200
  cat_dir = course_dir / item.category
198
201
  if item.category == "assignments" and item.name:
199
202
  m = HW_NUM.search(item.name)
@@ -202,7 +205,7 @@ def _category_dir(course_dir: Path, item: FileItem) -> Path:
202
205
  return cat_dir
203
206
 
204
207
 
205
- def _relpath(path: Path, course_dir: Path) -> str:
208
+ def relative_path(path: Path, course_dir: Path) -> str:
206
209
  """相对课程目录的展示路径;跨盘等取不到相对路径时退回文件名。"""
207
210
  try:
208
211
  return str(path.relative_to(course_dir)).replace("\\", "/")
@@ -262,12 +265,12 @@ class DownloadSession:
262
265
 
263
266
  状态取值:``downloaded`` / ``exists`` / ``would-download`` / ``empty``。
264
267
  """
265
- cat_dir = _category_dir(course_dir, item)
268
+ cat_dir = category_dir(course_dir, item)
266
269
  name, size = self.probe(item)
267
270
 
268
271
  already = _index_match(index, name, size)
269
272
  if already is not None:
270
- return ("exists", _relpath(already, course_dir))
273
+ return ("exists", relative_path(already, course_dir))
271
274
  target = cat_dir / name
272
275
  if target.exists():
273
276
  return ("exists", target.name)
@@ -344,5 +347,7 @@ __all__ = [
344
347
  "SIZE_DEDUP_THRESHOLD",
345
348
  "SNIFF_BYTES",
346
349
  "build_index",
350
+ "category_dir",
351
+ "relative_path",
347
352
  "sniff_ext",
348
353
  ]
@@ -87,7 +87,8 @@ def do_login(page: Page, username: str, password: str) -> None:
87
87
  raise AuthError(
88
88
  f"登录失败(当前 URL: {_safe_url(page)}):{detail}",
89
89
  f"已保存登录现场到 {home}(debug_login.png / debug_login.html)。"
90
- "若页面停在 ADFS / MFA / 验证码,请用 bb-sync run --headed 手动完成;"
90
+ "若页面停在 ADFS / MFA / 验证码,请用 bb-sync run --headed --keep-open 手动完成"
91
+ "(--keep-open 让浏览器在命令结束后留在屏幕上);"
91
92
  f"若怀疑是登录流程改版或 bb-sync 的问题,欢迎到 {ISSUES_URL} 提 issue,"
92
93
  "并附上 debug_login.html(以及上面这行 URL)。",
93
94
  ) from exc
@@ -161,7 +162,7 @@ def resolve_credentials(console: Console) -> Credentials:
161
162
  def ensure_login(page: Page, console: Console, *, headed: bool = False) -> None:
162
163
  """确保处于登录态(复用 profile → 静默登录 → 有头模式等人工过 MFA)。"""
163
164
  if is_logged_in(page):
164
- console.log("[login] 已有有效登录态(复用 Steel profile)")
165
+ console.log("[login] 已有有效登录态(复用浏览器 profile)")
165
166
  return
166
167
 
167
168
  creds = resolve_credentials(console)
@@ -76,6 +76,24 @@ class DueItem:
76
76
  }
77
77
 
78
78
 
79
+ @dataclass(frozen=True)
80
+ class SavedFile:
81
+ """本次同步新落盘的一个文件。"""
82
+
83
+ course: str # 课程代码;没有代码时退回课程标题
84
+ category: str # lectures / assignments / tutorials / other
85
+ name: str # 相对课程目录的路径,如 assignments/hw2/report.pdf
86
+ path: str = "" # 绝对路径,便于脚本直接取用
87
+
88
+ def as_dict(self) -> dict[str, str]:
89
+ return {
90
+ "course": self.course,
91
+ "category": self.category,
92
+ "name": self.name,
93
+ "path": self.path,
94
+ }
95
+
96
+
79
97
  @dataclass
80
98
  class SyncStats:
81
99
  """一次同步的统计结果。"""
@@ -86,6 +104,8 @@ class SyncStats:
86
104
  empty: int = 0
87
105
  failed: int = 0
88
106
  courses: list[str] = field(default_factory=list)
107
+ #: 本次真正新落盘的文件(不含已存在 / dry-run),供末尾明细与 --json 使用
108
+ files: list[SavedFile] = field(default_factory=list)
89
109
 
90
110
  def bump(self, status: str) -> None:
91
111
  key = status.replace("-", "_")
@@ -100,6 +120,7 @@ class SyncStats:
100
120
  "empty": self.empty,
101
121
  "failed": self.failed,
102
122
  "courses": self.courses,
123
+ "files": [saved.as_dict() for saved in self.files],
103
124
  }
104
125
 
105
126
 
@@ -139,5 +160,6 @@ __all__ = [
139
160
  "DueItem",
140
161
  "DueStats",
141
162
  "FileItem",
163
+ "SavedFile",
142
164
  "SyncStats",
143
165
  ]
@@ -0,0 +1,5 @@
1
+ """浏览器后端:直接驱动本机 Chrome / Edge(分离进程 + CDP)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []