snowy-yang
|
095171e0b7
|
refactor: 两个根级游离模块并入 music_api 包,消除分层倒置
为什么:podcast_api.py(560 行) 与 qr_login.py(397 行) 既不属于任何包、也不是
入口脚本,却同时被 services 层与 web 层引用——`services/downloads.py` 直接
`from qr_login import ...`、`services/podcast.py` 直接 `import podcast_api`,
业务层反向依赖顶层游离模块,是本次架构体检里唯一的无争议分层倒置。
两者本就依赖 music_api(前者取 url_v1/APIConstants/APIException,后者取
QRLoginManager),与 music_api 下已有的 qr.py / compat.py 是同一层的同类物。
迁移(git 保留重命名历史):
- podcast_api.py -> music_api/podcast.py
- qr_login.py -> music_api/qr_client.py
**命名避让**:不叫 music_api/qr_login.py——music_api/__init__ 已经从 .compat
导出了一个**同名函数** qr_login,同名子模块会与之互相遮蔽。改叫 qr_client
(其核心导出即 QRLoginClient),并在验证里专门确认 music_api.qr_login 仍是
可调用函数、music_api.podcast 是模块,两者不冲突。
内部导入顺带改为精确子模块,不再绕道包门面:
- podcast.py:from music_api import APIConstants/APIException/url_v1
→ from .compat import url_v1 / from .constants import ... / from .errors import ...
- qr_client.py:删掉那层 try/except ImportError 包装(它给的是「请确保
music_api.py 文件存在」这类顶层脚本时代的提示,作为包内模块已无意义),
改 from .cookies/.errors/.qr 相对导入
- CLI 用法文本 python qr_login.py → python -m music_api.qr_client
(相对导入后直接跑脚本会失败,必须走 -m;全仓确认无脚本/文档引用旧入口)
- 路径安全性已核对:qr_client 用的是 CWD 相对的 cookie.txt,不是 __file__ 相对,
迁移不改其解析结果
引用点全部更新(AST 扫描确认覆盖全集,替换后残留 0):
- podcast:services/podcast.py(1 + 5 处调用) · web/routers/podcast.py(1 + 6)
· web/routers/download.py(2 处函数内 + 3),导入名统一为 `podcast`
- qr_login:services/downloads.py · web/routers/admin.py · web/routers/user.py(×2)
- services/__init__.py 依赖自检列表去掉这两个名字(已并入 music_api)
- pyproject include 去掉两个顶层文件名(music_api 目录已覆盖)
验证:
- 11 + 3 个符号在新位置全部 hasattr 通过;podcast_api / qr_login 已
ModuleNotFoundError;6 个调用方模块均可 import;services 依赖自检仍生效
- 门禁:ruff 0 · pyright 0 errors/0 warnings · pytest 184 passed
- 真实 uvicorn 冒烟 10/10,含 /podcast/parse 与 /podcast/hot 返回 401
(模块导入失败会是 500,路由缺失会是 404,401 证明两者都正常)
- 生产 data/ 指纹 before == after
|
2026-09-26 15:03:47 +08:00 |
|
snowy-yang
|
7a70eef636
|
refactor: 删除 services.runtime 兼容门面,7 处调用点直连真实子模块
为什么:上一次分包重构把 1900 行巨石按职责拆成 9 个子模块后,留了一个
runtime.py 作为「原样再导出」的兼容门面,好让调用点零改动。但迁移期结束后
没有一个调用点迁走——门面成了永久中间层:每次读代码都要先跳一层才知道
符号真正住在哪,pyright 也无法从门面追溯定义位置。
改法:保持「函数内延迟导入」的形式不变,只把导入目标换成真实子模块。
**刻意不提到模块顶部**——services/__init__.py 里有依赖自检
(缺 auth_system/podcast_api/music_api/music_downloader/qr_login 就 sys.exit(1)),
提到顶层会让「import web.*」这一步就触发该自检,改变启动语义,超出本次范围。
7 处调用点(AST 精确扫描确认全集):
- web/routers/download.py ×4:download / download_batch_zip / podcast_download /
podcast_batch_zip,分别直连 services.downloads / batch / podcast / tasks
外加 _shared._build_download_base 与 api_service.api_service
- web/routers/tasks.py ×2:_semaphores 取 _shared.BATCH_ZIP_SEM + api_service,
topen 取 _shared.ENDPOINT_NAMES
- web/tasks_registry.py ×1:start_task 直连 _taskcore._start_task_thread
子模块短名统一加 `services_` 前缀别名(services_tasks 等),避免与函数内
既有局部名相撞。
顺带清理:9 个子模块 docstring 里「(自 services/runtime.py 拆出……)」的
历史说明在门面删除后已成悬空引用,一并去掉;包的模块划分说明改为
「不再有兼容门面层」。
验证:
- 符号可达:11 个被引符号在真实子模块上全部 hasattr 通过;
services.runtime 已 ModuleNotFoundError;api_service 仍是同一实例对象
- 三个调用方模块均可正常 import
- 门禁:ruff 0 · pyright 0 errors 0 warnings · pytest 175 passed
- 真实 uvicorn 冒烟 8/8(含 POST /auth/login 400),临时数据目录独立落库
9 张表,生产 data/ 指纹三项一致
|
2026-09-26 14:48:05 +08:00 |
|
snowy-yang
|
70ca21974b
|
refactor: 拆掉 auth_system 包内循环依赖,路径常量收敛到 db 单一来源
为什么:auth_system 的 11 个子模块此前构成一个强连通分量——db.py 与 backup.py
为了「动态读取路径常量」而在模块级 `import auth_system`(包自身),而包 __init__
又再导出这两个子模块的全部符号,互相指成环。
这个环当前并不崩(实测 8/8 种导入姿势全通过),它的代价是**双绑定**:
`auth_system.DB_PATH is auth_system.db.DB_PATH` 初始为 True,但只 patch 其一
另一处不跟随。于是测试不得不把 4 个路径常量**两处都 patch**
(setattr(auth,...) + setattr(db,...))——而漏 patch 的后果是静默写生产库,
commit fc7fc21 修的那次「黄金测试写进生产库」正是这么来的。
改法:让路径常量回归**模块属性单一来源**。
- db.py:删掉 `import auth_system`,内部改用本模块常量(DATA_DIR / DB_PATH /
SECRET_KEY_PATH)。这些常量本就定义在本模块,绕道包属性纯属多余。
- backup.py:改用 `import auth_system.db as db` 做模块限定访问,路径走
db.BACKUP_DIR / db.DB_PATH——保留动态读取能力,来源从「包属性」换成
「db 模块」,与唯一来源一致。
注意**不能**写成 `from . import db`:该形式在静态图里同时指向包本身与子模块,
会把这个环重新连上(实测确实如此,SCC 从 11 降到 2 就卡住了)。
- admin_site.py:5 处 `auth.DB_PATH` / `auth.BACKUP_DIR`(auth 是包别名)同样
改为经 db 模块访问。**这处不修就会出事**——它读的是包属性快照,而测试已改为
patch db 模块,于是备份上传会静默落到生产 data/backups/(本次实测真的发生了,
2 个 upload_*.db 已清理,副本留在临时目录)。
- 测试:7 个文件的 patch 目标从包属性迁到 auth_system.db.*。
- __init__.py:补注释说明这些常量在此只是 import 快照,禁止经包属性重定向。
验证:
- Tarjan 重算:非平凡强连通分量 11 → **0**(import 图已是 DAG)
- AST 全仓扫描:经包属性访问路径常量的点 **0 处残留**
- 行为:子进程内 patch auth_system.db.* 后,get_conn 连的库 / create_backup
落盘目录 / get_secret_key 目标路径**三者全部跟随**
- 导入姿势回归 8/8 通过
- 门禁:ruff 0 · pyright 0 errors 0 warnings · pytest 175 passed
|
2026-09-26 14:37:58 +08:00 |
|
snowy-yang
|
183f5744d8
|
fix: 修复 7 处变量可能未绑定的真缺陷,类型检查升到 standard
为什么:IDE 里成片的黄色波浪线并非风格噪声。ruff 与 pyright(basic) 都是
0 报错,但 basic 会整体漏报「变量可能未绑定」这类**可执行缺陷**;实测
standard 多报 7 处且全部为真——异常处理器引用了尚未赋值的局部变量,一旦走进
异常分支就会 NameError 二次崩溃,把原始异常彻底掩盖。
- core/db.py, core/http_client.py:3 处 @asynccontextmanager 的返回标注
AsyncIterator[X] 更正为 AsyncGenerator[X, None](前者不含 send 语义)
- core/state.py:三个模块级缓存补 TypedDict 建模。原先无标注的 dict 字面量被
推断为「所有值类型的并集」(float | None),每个读写点都退化成联合类型;
拆成 _VipCache(data 可为 None 表示未探测)与 _VipEntry(data 必定存在)后,
类型检查立刻抓出 account.py 对未探测缓存的直接解包隐患
- services/batch.py:zip_valid 提到函数头。ZIP 构建前的业务错误(参数缺失/
无歌曲)会 early raise,而异常处理器引用了该名字 → NameError 二次崩溃
- services/podcast.py:radio_name、pct 同理补初始化
- services/downloads.py:声明返回 str 却可能给出 None,改 `or ''` 满足契约
- web/routers/download.py:id_list 提前绑定
- web/auth_state.py:解包出的 c 未被使用
- pyproject.toml:typeCheckingMode 由 basic 升到 standard 作为门禁,并移除
已被 standard 覆盖的 reportGeneralTypeIssues
门禁:ruff All checks passed · pyright(standard) 0 errors 0 warnings
|
2026-09-26 13:54:23 +08:00 |
|
snowy-yang
|
6288a98ecc
|
refactor: 拆分 4 个巨石模块为包,清理过时注释并删除作废文档
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留
大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述,
会误导后来者按不复存在的架构去改代码。
拆分(对外导入路径不变,行为零变更):
- music_downloader.py -> music_downloader/(models/downloader/cover/_http/_log/_concurrency)
- auth_system.py -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...)
- music_api.py -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors)
- services/runtime.py -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/
streaming/tasks);services/runtime.py 保留为兼容门面
兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点
(auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性
动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。
修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字
等价」校验逐一定位,静态检查只会报出其中 4 处):
- core/config.py ).strip() or '*'
- core/state.py threading.Lock() x2
- qr_login.py datetime.now() / .strip()
- music_downloader/downloader.py .lower()
- services/podcast.py set() / time.time() x2
清理与删除:
- 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt
- 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等
失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明)
- _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含
- pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败
验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed;
真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
|
2026-09-26 12:37:48 +08:00 |
|
snowy-yang
|
838d635bc5
|
fix: 修复重构遗留缺陷,补齐 pyright 检查盲区
上一次重构(8446166)声称「pyright 0 错误」,但 pyproject 里 pyright 的 include
只覆盖 core/web/services —— 根目录 6 个应用模块与整个 tests/ 从未被检查,而
requests→sync_http 迁移的遗留恰好全部藏在那里。补齐 include 后立刻暴露
7 errors + 9 warnings,其中数处在运行时静默失效。
修复:
- services/runtime.py:删除 _podcast_billing_count/_billing_ids_count 两个
无调用方的死函数。它们仍调用 api_service._safe_get_request_data(),而该方法
在剥离 Flask 时已被删除,异常又被 except 吞掉 —— 一旦被调用,播客/歌单计费
恒为 1 次且不报任何错。路由层已各自实现同口径版本,故整体删除。
- web/routers/admin_site.py:备份上传的类型判定必须用 starlette 的 UploadFile。
fastapi.UploadFile 是它的子类,而 request.form() 返回基类实例,用错会让
isinstance 恒为 False,把每一次真实上传都判成「请选择数据库文件」。
已补 4 例真实 multipart 回归测试,并用变异测试确认能抓住该缺陷。
- auth_system.py:修正 create_user(实返 sqlite3.Row|None)与 list_cards
(实返 (cards,total) 元组)与实现不符的返回注解;注解一改正,立刻暴露出被它
掩盖的 register.py 中 int(user['id']) 未做 None 检查的 500 崩溃路径,已补守卫。
- music_downloader.py:修正注解引用的 core.sync_http.Response(正确名应为
SyncResponse);封面缓存淘汰不再读类型为 Optional 的 deque.maxlen,改用命名常量。
- services/runtime.py:zip worker 的 result.music_info 可能为 None,原样使用会
炸掉整批任务,改为按单曲失败处理。
- music_api.py:三处 isinstance 三元表达式收敛为 _as_dict() 帮助函数。
- web/main.py:文档串移除已删除的 Flask 兜底描述,补充 data/ 相对 CWD 解析的提醒。
验证:ruff check 通过;pyright 0 errors / 0 warnings(filesAnalyzed=67);
pytest 144 passed;真实 uvicorn 启动冒烟通过(/health、/、静态资源 200,
未鉴权管理端点 403)。生产库仅做只读 integrity_check,未被写入。
|
2026-09-26 11:52:06 +08:00 |
|
snowy-yang
|
8446166120
|
refactor!: uv 管理环境 + 彻底删除技术债(Flask 与 requests 全部移除)
## uv 环境管理
- pyproject.toml 声明 dependencies + [dependency-groups].dev,uv.lock 精确锁定
- 删除 requirements.txt / requirements-fastapi.txt / requirements-dev.txt
- .python-version = 3.12;Dockerfile 改用 uv sync --frozen(可复现构建)
- 新增 jinja2(首页渲染)
## 删除 Flask 技术债
- 从 main.py 用 ast 精确剥离 56 个 Flask 节点(路由/中间件/错误处理/启动),
框架无关的服务层落为 services/runtime.py(MusicAPIService、下载/打包 worker、
任务监控、缓存清扫),零 Flask 依赖
- 删除 main.py(4527 行)与 auth_routes.py(1050 行)
- auth_system.py 移除会话/请求上下文层(login_session/current_user/login_required/
admin_required/_client_ip 等),保留数据与业务层
- 新增 core/net.py(IP 解析)、web/auth_state.py(登录锁定/2FA 票据/发码限流)
- FastAPI 侧改直接依赖 services.runtime,不再 import main
- web/app.py 去掉 Flask fallback;入口默认根路径模式(API_PREFIX 可选)
## 删除 requests 技术债
- 新增 core/sync_http.py:requests 风格的同步门面(底层 aiohttp 连接池,
流式读取经有界队列背压),含 Session/headers/(connect,read) 元组超时兼容
- core/utils、auth_system、podcast_api、music_downloader、services 全部切换
- 实测真实下载 5.2MB 音频完整成功
## 质量门禁归零
- ruff:0 错误(B904 全部修复,不再忽略)
- pyright:0 错误(类型收窄修复)
- pytest:140/140(含 golden 契约回归、root 模式 66 项路径覆盖)
- 真实 uvicorn 启动:/health、/、/static、受保护端鉴权全部正确
新增 tests/golden/responses.json:删除 Flask 前捕获的 32 例响应快照,
FastAPI 逐字段复现——删除巨石后行为回归仍有硬证据。
|
2026-09-26 11:10:17 +08:00 |
|
snowy-yang
|
f5d1e3c3a9
|
feat: 支持纯 FastAPI 模式(根路径服务前端)+ 修任务端点路径
- web/app.py:create_app(api_prefix=) 参数化前缀——'' 即根路径模式,
87 条路由直接服务前端使用的路径,无需 Flask 兜底
- web/main.py:WEB_MODE=dual(默认,迁移期)/ fastapi(纯 FastAPI)切换
- 修真实路径缺陷:任务类端点漏了 /api 段(Flask 为 /api/topen 等,
原写成 /topen),根路径模式下前端会 404。已改为 /api/topen、/api/queue、
/api/batch_progress、/api/task/*;fe_error 同步修正
- 奇偶校验路径映射同步更新
验证:
- tests/test_root_mode.py 新增 66 项:前端调用的每条路径在纯 FastAPI 模式下
均非 404;root 模式不再暴露 /api/v1
- 纯 FastAPI 模式真实启动(uvicorn, PORT=5002)实测:
/health、/、/static 200,/song、/api/task/list 401(鉴权生效)
- 全套 pytest 135/135;ruff All checks passed;pyright 0 errors;奇偶 36/36
|
2026-09-26 10:25:05 +08:00 |
|
snowy-yang
|
cd0f1f78a3
|
refactor: 迁移收尾(管理员2FA)+ 抽取公共工具层,消除 router 间重复
补齐最后缺口:
- POST /api/v1/auth/admin_login/email、/auth/admin_login/verify(管理员 2FA Step2/3),
复用 auth_routes 的票据表与尝试上限(跨栈共享),签发会话 Cookie
重构(消除约 200 行重复,11 个 router 受益):
- web/request_utils.py:request_data / safe_int / extract_id / cookies / dedup_ids
原在各 router 各复制一份,现集中
- web/serializers.py:format_credits / user_public / record_req_log
- 各 router 改为导入别名,行为逐字不变
覆盖率:Flask 90 条业务路由全部有 FastAPI 等价端点(含 5 个向后兼容别名)。
验证:pytest 69/69;ruff All checks passed;pyright 0 errors;奇偶 36/36;冒烟 10/10。
|
2026-09-26 10:20:46 +08:00 |
|
snowy-yang
|
9df0028195
|
feat(routes): 迁移下载/批量打包/播客下载(账本事务)+ 修密钥绑定缺陷
迁移端点:
- POST /api/v1/download(单曲:file 后台任务 / json 同步交付+结算)
- POST /api/v1/download/batch_zip(按去重歌曲数计费)
- POST /api/v1/podcast/download、/podcast/download/batch_zip
- 新增 web/billing.py(预扣+开账同事务 / 只退 open 行 / rename 承接)
与 web/tasks_registry.py(任务登记/容量闸/账本撞名重试,与 main 同口径)
- worker 仍复用 main 既有实现(无 flask 依赖),账本与交付语义不变
修复我引入的真实缺陷:
- download 参数校验分支(ID/音质/格式非法)漏退款 → 预扣泄漏。改为统一 _fail() 退款
- web/routers/{auth,register}.py 在导入期绑定 deps.secret_key,导致 patch web.deps.secret_key
无效、且测试顺序相关。改为调用期 deps.secret_key() 解析
测试:tests/test_download_billing.py 7 项沙箱用例如实断言账本守恒
(预扣必退、无 open 行残留、次数不足不建任务);
全套 pytest 69/69(顺序无关);ruff All checks passed;pyright 0 errors;奇偶 36/36。
|
2026-09-26 10:13:16 +08:00 |
|
snowy-yang
|
fb40b2132c
|
feat(routes): 迁移管理后台全部 /admin/site/* 端点(28 个)
- web/routers/admin_site.py:用户管理(users/delete/duration/create/
reset_password/toggle/ban/cookie)、SMTP(get/save/test)、日志(logs/clear)、
卡密(generate/list/disable/delete)、公告(get/save)、播放密钥(get/rotate)、
数据库备份(create/list/download/upload/restore/delete/auto)
- 全量经 asyncio.to_thread 调用 auth_system 同步函数(SQLite/SMTP/文件),不阻塞事件循环
- 操作者名取当前管理员用户名;管理员闸 403 带 error_code='forbidden'(与 admin_required 同)
验证:11 项代表性端点与 Flask 逐字段一致(含参数校验与错误文案);
非管理员 403 一致;pytest 62/62;ruff All checks passed;pyright 0 errors。
|
2026-09-26 10:02:24 +08:00 |
|
snowy-yang
|
915358405c
|
feat(routes): 迁移注册/找回密码/卡密/验证码端点
- web/routers/register.py:auth/send_code、auth/register、auth/trial、
auth/redeem、auth/forgot_password
- 同步调用(SQLite 事务/SMTP/PBKDF2)走 asyncio.to_thread;发码 IP 限流复用
auth_routes._SEND_CODE_IP(跨栈共享计数)
- 注册成功路径签发会话 Cookie;管理员找回密码仍防枚举拦截
测试:tests/test_register.py 6 项沙箱用例(播种验证码绕 SMTP)——
协议门槛、注册成功建号+赠送+会话可用、验证码错误、找回重置后可登录、
管理员找回被拦、卡密充值。
全套:pytest 62/62;ruff All checks passed;pyright 0 errors;冒烟 10/10。
|
2026-09-26 10:00:39 +08:00 |
|
snowy-yang
|
22f4d77abd
|
feat(routes): 迁移管理员扫码/Cookie/操作记录端点
- web/routers/admin.py:admin/qr/create、admin/qr/check、admin/cookie/status、
admin/cookie/delete、admin/logs
- 管理员闸用本地实现,文案与「无 error_code」包体与 Flask _verify_admin_password
逐字对齐(与 auth_system.admin_required 的 forbidden 字段不同)
- QRLoginManager await;文件类操作(save_cookie/logout/_get_cookie_info) 走 to_thread
- Cookie 变更后同步清 VIP 与全局 MUSIC_U 缓存
契约奇偶校验:36 项全 OK(新增 admin:logs、admin:cookie_status、非管理员 403);
ruff All checks passed;pyright 0 errors;冒烟 10/10。
|
2026-09-26 09:59:04 +08:00 |
|
snowy-yang
|
b7616e9831
|
feat(routes): 迁移个人中心/公开播放/前端错误端点 + 修 QR 异步回归
迁移(/api/v1/*,与 Flask 逐字段对齐):
- user/qr/create、user/qr/check、user/status、user/logout、user/playlists、
user/playlist、user/collect、user/comment
- api/playlist/play(密钥校验 + 逐曲直链解析:ThreadPoolExecutor(4) → asyncio.gather(4))
- fe_error(前端错误上报,10/分/IP + 字段清洗)
修复我此前引入的异步回归(重要):
- music_api.QRLoginManager 异步化后,main.py 4 处 Flask 同步路由仍当同步调用,
返回协程 → Flask 侧扫码登录必坏。已改经 run_sync 桥接,实测两栈均返回 SVG。
- web/routers/user.py 同样改为 await。
契约奇偶校验:33 项(新增 user:status/logout/playlists401/playlist401/play:badkey);
ruff All checks passed;pyright 0 errors;冒烟 10/10。
|
2026-09-26 09:57:42 +08:00 |
|
snowy-yang
|
91c032a1c3
|
feat(routes): 迁移认证端点(login/logout/heartbeat/ann_seen)
- web/routers/auth.py
· login:邮箱/用户名登录、超长密码短路、失败锁定、封禁/禁用分支、
管理员转 2FA 票据;成功签发 core.session 会话 Cookie(与 Flask 同格式)
· logout:递增 login_seq 顶号 + 删 Cookie
· heartbeat:次数校验(401/expired 语义同 Flask)
· ann_seen:记录已读公告版本
· 失败锁定与 2FA 票据表复用 auth_routes 模块级状态(跨栈共享,防绕锁定)
· PBKDF2 与库操作走 asyncio.to_thread,不阻塞事件循环
测试:tests/test_auth_routes.py 5 项,全部使用**沙箱库**(pytest tmp_path +
monkeypatch auth_system/core.db 路径),不触碰生产 data/site.db。
全套:pytest 56/56;ruff All checks passed;pyright 0 errors;奇偶校验 28/28。
|
2026-09-26 09:54:14 +08:00 |
|
snowy-yang
|
18c77296c2
|
feat(routes): 迁移首页/静态资源 + /auth/me 认证状态
- web/routers/index.py:GET / 首页,用 Jinja2 以 Flask 同口径渲染
(keep_trailing_newline=False)——输出与 Flask render_template 逐字节一致
- web/app.py:挂载 /static 静态资源(site_auth.js 等,与 Flask 同目录)
- web/routers/auth_status.py:/api/v1/auth/me 认证状态
· user/duration_text/announcement/smtp_configured/register_credits 逐字段对齐
- core/repo.py:补 get_announcement / get_smtp_config / smtp_configured 异步读
契约奇偶校验:28 项全 OK(新增 index 逐字节、auth:me);
验证:pytest 51/51;ruff All checks passed;pyright 0 errors;
uvicorn 实跑 / 与 /static 与 /api/v1/* 均 200。
|
2026-09-26 09:51:32 +08:00 |
|
snowy-yang
|
0104ae55e5
|
feat(routes): 迁移音频流代理与结果交付端点(流式)
- web/routers/stream.py:/api/v1/audio_stream 音频范围流代理
· 签名校验(core.security,密钥同源两栈互认);HEAD 直接 200 不请求上游
· Range 原样透传上游(206 + Content-Range,拖动进度可用)
· 编码透明:上游带 Content-Encoding 时不沿用压缩前长度(同 Flask F92)
· StreamingResponse 16KB 分块,finally 关闭上游(下游中断亦释放)
- core/http_client.py 增 open():返回未关闭流式响应,便于先读状态码再转发
- web/routers/delivery.py:/download/result、/download/batch_zip/result
· 归属 403 / 进行中 202 / 暂停 200 / 取消 200 / 失败 500 / 缺失 404 分支逐一对齐
· FileResponse 流式交付 + no-store + X-Download-Filename;清理由监控线程统一回收
验证:pytest 51/51(新增 5 项流代理:无效 token/HEAD/全量/Range 透传/上游关闭);
ruff All checks passed;pyright 0 errors;奇偶校验 26/26;实测 5.2MB 流与
Range 206 正确。
|
2026-09-26 09:49:22 +08:00 |
|
snowy-yang
|
f588d313f0
|
feat(routes): 迁移任务状态端点(topen/queue/batch_progress/task/*)
- web/routers/tasks.py:topen、queue、batch_progress、task/cancel、
task/heartbeat、task/list 迁至 /api/v1/*
- 读写 core.state 共享任务表/统计(与 Flask 同一批对象)——两栈迁移期看到同一份
任务状态,这是单进程双框架成立的关键
- task/cancel 保持 POST-only(与 Flask 同口径,杜绝顶层导航 GET 触发取消)
契约奇偶校验:24 项全 OK(新增 queue/topen/task:list/heartbeat/progress404);
验证:pytest 46/46;ruff All checks passed;pyright 0 errors;冒烟 10/10。
|
2026-09-26 01:18:51 +08:00 |
|
snowy-yang
|
f45a9ccd3a
|
feat(routes): 迁移播客查询端点(parse/programs/search/hot)
- web/routers/podcast.py:四个播客端点迁至 /api/v1/*,与 Flask 逐字段对齐
- podcast_api 仍为同步模块,用 asyncio.to_thread 包装:阻塞上游不占事件循环,
行为与 Flask 一致
- 单集解析附音频直链(音质自动降级)+ 解析操作记共享 REQ_LOG
契约奇偶校验:19 项全 OK(新增 podcast hot/search/programs);
play_count 加入易变字段(上游实时收听计数每次请求都变,非契约漂移)。
验证:pytest 46/46;ruff All checks passed;pyright 0 errors;冒烟 10/10。
|
2026-09-26 01:16:53 +08:00 |
|
snowy-yang
|
6c3181341b
|
feat(routes): 迁移账号/全局歌单端点 + 鉴权依赖测试
迁移路由(/api/v1/*,与 Flask 同端点逐字段对齐):
- account/vip:admin/user 双路径 + 5 分钟缓存(降级结果 30s),
VIP 探测单飞锁与 Flask 同源跨栈共享;未登录可访问(同 Flask)
- global/playlists、global/playlist:管理员闸(require_admin,403 对齐)
- web/deps.py 新增 require_admin;_resolve_user_cookies 支持可选用户(匿名审计保底)
测试:46/46(新增 10 项鉴权依赖:有效/管理员/封禁/禁用/顶号/伪造 Cookie、
401/403 闸);ruff All checks passed;pyright 0 errors。
契约奇偶校验:16 项全 OK(新增 account:vip、global:playlists/playlist、
非管理员 403);uvicorn 实跑双栈 account/vip 均 200、未授权歌单 403。
|
2026-09-26 01:15:22 +08:00 |
|
snowy-yang
|
a2758aa0da
|
feat(routes): 迁移解析端点 /song /playlist /album + 响应卫生中间件
迁移路由(/api/v1/*,与 Flask 同端点逐字段对齐):
- song(type: url/name/lyric/json)、playlist、album
个人账号路径实现全局账号冒用拦截(同 Flask 口径);json 模式 asyncio.gather 并发
song/url/lyric(歌词失败不阻断,与 Flask 线程池语义一致)
- core/media.py:format_file_size/quality_display_name/parse_cookie_str 纯函数复用
web/middleware.py(镜像 Flask after_request):
- 音频直链 → HMAC 签名代理地址(密钥同源,两栈 token 互认)
- 安全头 nosniff/DENY/Referrer-Policy/CSP 基线;Server 抹为 NeteaseUrl;429 带 Retry-After
- 纯 ASGI 逐段转发(不破坏流式下载);JSON 200 延迟 start 并**重算 Content-Length**
——修实测 IncompleteRead(原实现转发旧长度 + 新 body)
契约奇偶校验:12 项全 OK(新增 album/playlist/song×3);
token/CDN 主机归一化(均为易变值,非契约漂移)。
验证:pytest 36/36;ruff All checks passed;pyright 0 errors;
uvicorn 实跑 /api/v1/song 直链已代理、CSP/Server 头正确。
|
2026-09-26 01:12:54 +08:00 |
|
snowy-yang
|
2c55b55e03
|
feat(routes): 迁移查询类路由到 FastAPI + 会话互通 + 统一错误/限流
会话互通(迁移关键):
- core/session.py:itsdangerous 兼容的签名 Cookie 编解码(同密钥/同 salt/同
时间戳编码),Flask 与 FastAPI 双向可读——实测 F->core 与 core->F 双向解出一致,
篡改/错密钥返回 None
FastAPI 基础设施:
- web/deps.py 会话→当前用户(封禁/顶号校验,口径同 auth_system.current_user)
- web/errors.py AppError + 全局异常处理,404/405/400 统一为本站包体
- web/ratelimit.py 复用 core.state 同一批限流桶(跨栈共享,避免绕框架绕过限流)
- core/repo.py 补 get_user_by_id(异步)
迁移路由(/api/v1/*,与 Flask 同端点逐字段对齐):
- toplist / artist_songs / comments / search,走 music_api 异步方法
- search 的服务器昵称遮蔽改为 FastAPI 侧异步实现(不再依赖 main)
契约奇偶校验:扩展至 7 项(含登录会话互通 + 未登录 401),全 OK;
CDN 分片主机差异归一化(上游易变值,非契约漂移)。
验证:pytest 33/33;ruff All checks passed;pyright 0 errors;
uvicorn 实跑未登录 401 包体与 Flask 一致。
|
2026-09-26 01:09:01 +08:00 |
|
snowy-yang
|
0f2e72db44
|
feat(db-async): 站点数据异步读写仓储 + FastAPI 生命周期
- core/repo.py:经由 core.db(aiosqlite) 读写 auth_system 的**同一份** site.db
(settings/announcements/users 只读/ledger 只读),字段口径对齐 auth_system;
读并发、写串行、WAL 读写同时进行
- web/routers/site.py:/api/v1/site/announcements、/api/v1/site/stats(异步多表并发读)
- web/app.py:lifespan 关闭时释放 aiohttp 连接池(含按 loop 隔离的 session)
写路径说明:迁移期写操作仍归 auth_system 的 _DB_LOCK 事务口径,避免两套写
路径互相踩踏;本层先只做读。
验证:pytest 22/22(新增 6 项含「异步读 + 同步写者并发 WAL」);
/api/v1/site/* 实测读到共享库(3 用户/1 管理员);ruff/pyright 干净。
|
2026-09-26 00:34:11 +08:00 |
|
snowy-yang
|
9f56f68b66
|
chore(async-infra): 异步基础层 + 工具链 + 清理
异步基础层(core/):
- db.py aiosqlite + WAL:读路径无锁并发、写路径 asyncio.Lock 串行,
busy_timeout 自旋——实现「读写同时进行」(旧实现用 RLock 全读串行)
- http_client.py aiohttp 连接池 + 有界重试(5xx 重试 / 4xx 不重试)+
响应体上限 + 流式接口;按事件循环隔离 session(支持迁移期双 loop)
工具链:
- pyproject.toml ruff(E/F/W/I/B/UP/C4/ASYNC/S/RUF) + pyright(basic) + pytest(asyncio auto)
- requirements-fastapi.txt FastAPI/aiohttp/aiosqlite/aiofiles 等目标栈
- requirements-dev.txt 开发依赖(pytest/ruff/pyright)
清理:删除无用脚本 _csscheck.py 与产物标记 _css_ok.txt/_pkg_ok.txt,同步 .gitignore
验证:pytest 10/10(含「写 200 条同时 4 读协程零异常」并发用例);
ruff All checks passed;pyright 0 errors。
|
2026-09-26 00:24:00 +08:00 |
|
snowy-yang
|
41356e89c4
|
feat(web): FastAPI 脚手架(strangler 迁移起点)
新增 web/ 包,与 Flask 应用并存、共享 core/:
- web/app.py create_app 工厂;已迁移路由挂 /api/v1,可挂遗留 Flask 兜底
- web/main.py uvicorn 入口(sys.path 补项目根)
- web/response.py 响应工具,与 Flask APIResponse 的 JSON 契约逐字段对齐
- web/routers/system.py 迁移样板:/api/v1/health、/api/v1/api/info
- requirements-fastapi.txt 迁移目标依赖(不影响当前生产 Flask 部署)
验证:
- 奇偶校验(.zcode/refactor/parity.py)Flask /health vs FastAPI /api/v1/health、
/api/info vs /api/v1/api/info —— 状态码/顶层键/响应体全一致
- uvicorn 真实启动 200;Flask fallback 挂载下 /api/v1/* 走 FastAPI、/* 走 Flask 同为 200
当前仅样板端点;生产服务器切换与状态统一见后续迁移阶段。
|
2026-09-26 00:17:56 +08:00 |
|