snowy-yang
|
064a89d726
|
refactor: 删除旧前端单体,React 产物成为唯一首页来源
为什么:React 迁移已验证完毕(浏览器冒烟全过、golden 刷新),
legacy 模板兜底分支指向已删除的文件只会制造幻觉路径。删除
templates/index.html(466KB)与 static/site_auth.js(80KB)及
/static 挂载;jinja2 随兜底分支失去唯一使用者,连同 markupsafe
一并移出依赖(63 包 -2)。_render_index 简化为直接读构建产物——
dist 已入库恒存在,无需分支。
|
2026-09-27 18:41:30 +08:00 |
|
snowy-yang
|
8803444755
|
feat: 后端接线上 React 构建产物,legacy 模板降级为兜底
为什么:前端迁移到 Vite 后,首页应直接返回 frontend/dist/index.html
字节(golden 口径随之变为构建产物原样字节),仅当 dist 未构建时
回退 Jinja2 渲染的 legacy 模板。同步挂载 /assets 静态目录、排除
frontend 的 node_modules/tsbuildinfo 入库、docker 构建只带 dist
不带源码、ruff/pyright 排除 frontend 目录(TS 侧由 tsc+vite 把关)。
|
2026-09-27 18:35:04 +08:00 |
|
snowy-yang
|
6fb37534d3
|
refactor: 计费退款不变量收敛为 Billing.scope()(download.py 25 处手工退款 → 0)
为什么:「任何失败路径必须退款」此前靠路由里每条早退/except 手工调
refund_open 维持(download.py 单文件 25 处),上一轮拆 register/login
时已经体会过这个模式的代价——改任何一条返回路径都要核对还原时序,
漏一处不会有测试报警,只剩重启对账兜底在跑。
改动:
- Billing.scope()(asynccontextmanager):作用域退出时自动退还仍 open
的账本行。正确性完全依赖 refund_open 的既有幂等口径(只退仍 open
的行)——settle()/rename_to() 消费过的行天然豁免,成功路径零动作,
无需手动「取消自动退」;未捕获异常先退款再原样穿透。
- download.py 四个端点全部改用 async with billing.scope(),删除全部
手工退款与 _fail 收尾闭包;_sync_download_json 的内部退款一并移除
(docstring 标明必须在 scope 内运行)。
- 语义逐路径等价:错误早退→自动退(同原手工);register 成功后
rename→自动退 no-op(同原手工退的 no-op);start 失败行挂 task_id
留给对账(同原);CancelledError 语义不变(退款随取消中断,对账兜底)。
先写测试后动代码:tests/test_billing_scope.py 5 例(TDD 红→绿),
对真实沙箱账本断言——open 行自动退、settle 后 no-op 且余额恰 -1、
rename 后请求级行消失且任务级行保持 open、异常穿透、free 路径零动作。
既有 30 例计费/下载端到端测试作迁移回归网。
门禁:ruff 全过 · pyright 0/0 · pytest 387 passed(382→387)。
|
2026-09-27 12:13:00 +08:00 |
|
snowy-yang
|
be4dd0646a
|
refactor: /download 同步分支拆解(120 行 / 圈 19 → 78 行 / 圈 14)
为什么:单曲下载提交入口的 format=json 同步分支内嵌了完整事务
(取信息 → 下载 → 组响应 → 结算/退款,40 行),与参数校验、后台任务
登记混在一个函数里;可选参数解析 5 行两模式共用却内联两处。
拆出:
- _parse_download_options:两模式共用的可选参数解析(lyric_format
非法静默回退 lrc 的历史口径保留)
- _sync_download_json:同步下载事务整体——任一步失败退预扣(账本
守恒),只有成功才 settle(1, 'delivered'),docstring 写明语义
先写测试后动代码:新增 tests/test_download_sync.py 5 例,在未改动
实现上跑绿(修正 1 例预期:默认 filename_format=artist_song 产出
「歌手 - 歌名.mp3」),覆盖此前零测试的 json 同步分支——成功结算
余额恰 -1、取信息失败 404 退款、下载失败/异常退款、空 ID 专属文案。
门禁:ruff 全过 · pyright 0/0 · pytest 375 passed(370→375)。
|
2026-09-27 11:11:56 +08:00 |
|
snowy-yang
|
f0de0b65b1
|
refactor: login 端点拆解(90 行 / 圈 26 / 嵌套 2 → 39 行 / 圈 10 / 嵌套 1)
为什么:登录是全部拒绝路径最密的端点(超长密码/锁定/密码错/封禁/
禁用/管理员无邮箱/异地封禁七道闸),90 行单函数把「身份解析、锁定
计数、状态闸、2FA 转票据、会话建立」五件事揉在一个 try 里,改任何
一道闸都要在嵌套分支里核对上下文。
拆出五个成员(顺序 = 原执行顺序,逐条语义对应):
- _resolve_login_identity:邮箱→账号解析 + 锁定键(保留两处判定
差异:锁定键用真值判断,verify_name 用 is not None——历史口径,
契约测试分别钉住)
- _allow_login_attempt:锁定闸 + 一次性审计日志(锁定周期内不重复落)
- _check_account_status:封禁(原因落日志)/禁用,通过返回 None
- _admin_two_factor_response:2FA 转票据,未绑定邮箱 fail-closed
- _user_session_response:login_seq 顶号 + 异地多省封禁闸(封禁时
再递增一次顶掉在途会话)+ Cookie 签发
先写测试后动代码:新增 tests/test_login_flow.py 9 例,在未改动实现上
一次跑绿,覆盖此前零测试的路径——超长密码不碰 PBKDF2(monkeypatch
verify_login 断言不被调用)、邮箱登录按归一化地址锁定、锁定日志幂等、
封禁/禁用文案、管理员 fail-closed、异地封禁双递增、非预期异常 500。
门禁:ruff 全过 · pyright 0/0 · pytest 348 passed(339→348)。
|
2026-09-27 10:13:36 +08:00 |
|
snowy-yang
|
5ad2152acc
|
refactor: register 端点拆解(84 行 / 圈 34 → 58 行 / 圈 20 / 嵌套 1)
为什么:注册是站点唯一的建号入口,84 行里五条「建号前失败」路径各自
手写一遍 restore_verify_code(有的还带失败日志),防刷双闸与三种建号
异常的口径全埋在分支里,改任何一条都要通读全函数核对还原时序。
拆出(顺序 = 原执行顺序,逐条语义对应):
- _validate_registration_input:入参四道闸(协议/用户名/密码/邮箱),
按序返回第一条错误文案
- _reject_registration:统一拒绝出口——还原已消费验证码(可选失败
日志);五处 restore 收敛为一处,docstring 写明「为何必须还原」
- _register_credits:赠送闸(同设备重复注册归零;顺带删除
credits<0 死分支——_safe_register_credits 已保证非负)
- _issue_registration_session:成功收尾(login_seq 递增 + 日志 + Cookie)
先写测试后动代码:新增 tests/test_register_flow.py 9 例,在未改动
实现上一次跑绿。验证码由 check_verify_code DELETE 原子消费,测试用
「失败路径后 email_codes 行仍在」做 restore 实证断言(不 mock 还原);
其中用户名重名路径用端到端方式验证——同一验证码不重发即可二次注册。
覆盖此前零测试的路径:IP 闸、设备赠送闸、IntegrityError/ValueError/
读回 None 三种建号异常映射、非预期异常 500 泛化文案。
门禁:ruff 全过 · pyright 0/0 · pytest 339 passed(330→339)。
|
2026-09-27 10:05:20 +08:00 |
|
snowy-yang
|
f27bd7dd74
|
refactor: /song 端点拆解(99 行 / 圈 18 → 主函数 27 行 / 圈 8)
为什么:song 是端点层最重的函数——参数校验(4 条)、账号 Cookie 选择、四种 type
分发、json 分支的三源合并与降级全塞在一处;而四种 type 的返回体形状与降级口径
**没有任何测试记录**(golden 里的 /song 只有一条参数校验用例 song_badid)。
改法:
- _parse_song_query:提取 + 校验,返回 _SongQuery 或**错误消息字符串**。返回
字符串而非响应对象,是为了让它不依赖响应层、可单独断言(校验段 4 条错误都是
默认 400,无需状态码)。
- _SongQuery:frozen dataclass。music_id 保持 extract_id 的**字符串原值**——它
原样进响应体 id 字段、golden 按字符串比对;上游要的 int 走 .mid property。
- _song_url_response / _song_json_response:两个实体分支各自成函数。
- song 主函数只剩「校验 → 选 Cookie → 四路分发」。
刻意保留的语义(是决定,不是疏漏):
- 解析日志**只在 json 分支**记录,url/name/lyric 三路不记——历史实现如此。
已写进 _song_json_response 的 docstring:不要为了「统一」把它上提到主函数,
那会新增三路的可观察副作用。
- 校验顺序(缺参 → ID 非数字 → 音质 → 类型)原样保留,并用测试钉住「同时传错
时报哪条」。顺序是用户可见行为。
- 三条 404 降级(上游 data 为空 / data=[null] / songs 为空)逐条保留。
复杂度(同口径 AST 实测,重构前后):
song 99 行 / 圈 18 / 嵌套 4 → 27 行 / 圈 8 / 嵌套 2
_song_json_response 新增 → 49 行 / 圈 6 / 嵌套 1
_parse_song_query 新增 → 21 行 / 圈 5 / 嵌套 1
文件内 >80 行函数 1 → 0;最大圈 18 → 11(余下最大的是本次未动的 mask_server_nickname)
注:上一轮口头报的「圈 44」与本轮口径不同,此处以本轮统一口径为准,重测为准绳。
等价性证据(三道,互相独立):
1. AST 字面量审计:字符串字面量 **227 处完全一致**;数值唯一差异是新增的
dataclass frozen=True。即没有一条用户可见文案或阈值被改动。
2. 反向注入验证:把 HEAD 的旧 song 换回去,用同一套 tests/test_song_types.py 跑
→ 19 例**全过**,证明这批测试忠实编码了旧行为、对新实现构成等价性验证。
3. 新增 19 例行为契约测试:四种 type 正路、json 的三源合并与三种降级(歌词失败
不阻断 / 直链失败置空 / 详情失败 404)、校验优先级、分享链接解析、
account=user 无 Cookie 401、全局 Cookie 冒用拦截。
顺带修掉一个测试自身缺陷:新用例里「个人 Cookie 与全局账号一致」一条**单跑通过、
全套失败**——根因是它依赖 GLOBAL_MUSIC_U_CACHE(5 分钟粘性的模块级缓存),别的
文件先跑会把值填上,于是拿到缓存值而非本处 patch 的 Cookie。已改为 fixture 里
用 monkeypatch.setitem 显式重置该缓存,让用例自足。
验证:ruff 全过 · pyright 0/0 · pytest 233 passed(214→233)· 生产 data/ 未被
触碰(全部用例走 tmp_path 沙箱)。
|
2026-09-27 07:41:16 +08:00 |
|
snowy-yang
|
1973d6a0e8
|
fix: 启用会话过期(服务端校验签发时间,堵住旧 Cookie 永不过期)
为什么:签发侧 Cookie 的 max_age 是 7 天,但那只是**浏览器**行为——被保存、
复制或从日志里捞出来的旧 Cookie 仍带着有效签名,而解析侧 loads 从不校验签发
时间(max_age=None),等于「登录态永不过期」。登出/顶号靠递增 login_seq 兜底,
但拿不到库的一方(如泄露的 Cookie)无从失效。
改法:
- core/session.py 新增 COOKIE_NAME / SESSION_MAX_AGE,作为会话名与有效期的
唯一来源。此前 'session' 在 web/deps、web/routers/auth、web/routers/register
各写一遍,7 天在两处各写一遍——三处同名常量各改各的,正是漂移温床。
- web/deps._parse_cookie 传入 max_age=SESSION_MAX_AGE,过期判定落在服务端。
- 两个签发侧改引用共享常量(保留模块级别名,对外名字不变)。
- loads() 的签名与语义**不变**(max_age=None 仍不校验):它是通用工具,策略
属于调用点,这样 test_session 里「None 接受旧时间戳」的语义继续成立。
行为变更(本轮唯一一处):签发时间超过 7 天的会话 Cookie 现在按未登录处理。
正常浏览器用户无感——浏览器本来就按 max_age 丢弃它;受影响的是手工保存、
跨 7 天复用的旧 Cookie,正是要堵的那类。
验证:
- 新增 tests/test_deps.py::test_expired_cookie_rejected:用 monkeypatch.context()
把时钟拨回 SESSION_MAX_AGE 之前签发(itsdangerous 时间戳取自 dumps 时刻),
再以当前时间解析 → 必须拒绝;同时断言有效期内不受影响。用 context 而非
undo(),后者会连带撤销 site fixture 的设置。
- 变异验证:把 max_age 参数摘掉后**恰好**该用例变红、其余 10 例全绿——证明测试
精确盯着这一处,不是碰巧通过。
- ruff 全过 · pyright 0/0 · pytest 214 passed(213→214)。golden 快照与
test_download_billing(真实登录拿 Cookie 打 API)均通过,端到端链路未受影响。
|
2026-09-27 07:32:05 +08:00 |
|
snowy-yang
|
3de75f7479
|
refactor: 两个「数据泥团」收成 dataclass / 字典
为什么:上一轮报告「11 个参数 > 6」的清单实测只有 7 个 ≥7 参数,其中真正
该改的是 2 个(另 5 个是 HTTP/下载原语的参数透传,合并反而降低清晰度,有意
保留)。
1) auth_system.create_user(11 参 → 6 参)
前 5 个是账号属性(username/password/email/credits/role,同属一个概念),
后 6 个才是泥团——它们是「注册风控上下文」(同 IP 频率闸 + 设备赠送闸)。
收成 frozen dataclass RegistrationGuard,默认值等价于「不做任何风控」,故
老调用点语义不变。
2) web/routers/download.py._single_task(11 参 → 2 参)
11 个位置参数改成一个 params 字典 + device_id,字典原样透传给 worker。
顺带删掉原本就**未被使用**的 data 参数(11 个里有 1 个是死的)。
理由不是「参数多」而是这 11 个位置参数没有名字,调用点极易错序。
教训:create_user 签名重构时漏改 web/routers/admin_site.py:155,pytest 189 例
全绿(该端点无测试覆盖)但 pyright 报出 2 errors —— 两道门禁互补的实证,
说明类型检查不是冗余。
|
2026-09-26 18:54:17 +08:00 |
|
snowy-yang
|
445c16c329
|
refactor: web/routers/tasks.py 的 _touch_device 收敛到 services 版
为什么:_touch_device 在 services/tasks.py 与 web/routers/tasks.py 各有一份
逐字相同的实现,且两份都活着。同一份「更新设备心跳时间」的逻辑有两处副本,
改一处忘另一处就会出现行为分叉。
改法:删掉 web 层本地副本(16 行),改为调用 services.tasks._touch_device
(两处调用点),并补 import asyncio 以包裹 to_thread。
说明:上一轮报告里提到的「6 组分叉实现」实测需逐组降级——success()/error()
分属不同层(一个是 tuple 返回、一个是 JSONResponse),iter_content 是两个垫片
的适配器,都**不该**合并;只有 _touch_device 是真·逐字重复,故只收敛这一组。
|
2026-09-26 18:54:04 +08:00 |
|
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 |
|