Archived
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/ 指纹未变;忽略注释的逐字等价性校验通过。
This commit is contained in:
1 parent
838d635bc5
commit
6288a98ecc
89 files changed
+5806
-5112
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""账号与全局歌单端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""账号与全局歌单端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST/GET /api/v1/account/vip ↔ Flask /account/vip(admin/user 双路径 + 缓存)
|
||||
- POST/GET /api/v1/global/playlists ↔ Flask /global/playlists(管理员)
|
||||
- POST/GET /api/v1/global/playlist ↔ Flask /global/playlist(管理员)
|
||||
@@ -31,7 +31,7 @@ _VIP_NAMES = {
|
||||
0: '普通用户', 1: '音乐包', 11: '黑胶VIP', 100: '黑胶VIP', 10: '黑胶SVIP', 110: '黑胶SVIP',
|
||||
}
|
||||
|
||||
# 探测单飞:与 Flask _VIP_PROBE_LOCK 同源(跨栈共享,避免两栈各自串行 8 级探测)
|
||||
# 探测单飞:与 core.state 中同一把锁共享,避免并发请求各自串行 8 级探测
|
||||
_VIP_PROCESS_LOCK: Any = VIP_PROBE_LOCK
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def _cookies() -> dict[str, str]:
|
||||
|
||||
|
||||
async def _detect_vip(cookies: dict[str, str]) -> tuple[int, str, list[str]]:
|
||||
"""检测账号会员类型与可解析音质(与 Flask _detect_vip 同口径)。
|
||||
"""检测账号会员类型与可解析音质。
|
||||
|
||||
单飞锁为 threading.Lock:异步上下文里用 to_thread 有界等待 0.5s,
|
||||
拿不到锁的请求直接用账号接口给降级结果(不等 8 级探测)。
|
||||
@@ -100,7 +100,7 @@ async def account_vip(
|
||||
'vip_name': _vip_name(vip_type), 'detected_by': method, 'levels_ok': levels_ok,
|
||||
'nickname': (info or {}).get('nickname', ''),
|
||||
}
|
||||
# 降级结果(levels_ok 空)短缓存 30s,正常结果 300s(与 Flask 同口径)
|
||||
# 降级结果(levels_ok 空)短缓存 30s,正常结果 300s
|
||||
ttl = 30 if not levels_ok else 300
|
||||
if len(VIP_USER_CACHE) > 500:
|
||||
VIP_USER_CACHE.clear()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""管理员扫码登录 / Cookie 管理 / 操作记录(FastAPI 栈)。
|
||||
"""管理员扫码登录 / Cookie 管理 / 操作记录。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST /api/v1/admin/qr/create ↔ Flask /admin/qr/create
|
||||
- POST /api/v1/admin/qr/check ↔ Flask /admin/qr/check
|
||||
- POST /api/v1/admin/cookie/status ↔ Flask /admin/cookie/status
|
||||
- POST /api/v1/admin/cookie/delete ↔ Flask /admin/cookie/delete
|
||||
- POST /api/v1/admin/logs ↔ Flask /admin/logs
|
||||
|
||||
鉴权:Flask 这些端点用 _verify_admin_password(文案"需要管理员权限,请使用管理员账号登录"),
|
||||
鉴权:管理员密码闸(文案"需要管理员权限,请使用管理员账号登录"),
|
||||
与 auth_system.admin_required 的文案不同,故本模块用本地 admin 闸保持逐字对齐。
|
||||
|
||||
说明:QRLoginManager 已异步化(直接 await);QRLoginClient 的 save_cookie/logout/
|
||||
@@ -34,7 +34,7 @@ logger = logging.getLogger('web.admin')
|
||||
|
||||
|
||||
async def _require_admin(user: Annotated[SiteUser | None, Depends(current_user)]) -> SiteUser:
|
||||
"""管理员闸(文案与 Flask _verify_admin_password 逐字对齐)。"""
|
||||
"""管理员闸(文案与历史实现逐字对齐)。"""
|
||||
if user is None or not user.is_admin:
|
||||
raise AppError('需要管理员权限,请使用管理员账号登录', 403)
|
||||
return user
|
||||
@@ -52,7 +52,7 @@ def _admin_client():
|
||||
|
||||
|
||||
def _reset_caches() -> None:
|
||||
"""Cookie 变更后清 VIP 探测缓存与全局 MUSIC_U 缓存(与 Flask 同口径)。"""
|
||||
"""Cookie 变更后清 VIP 探测缓存与全局 MUSIC_U 缓存。"""
|
||||
VIP_CACHE['data'] = None
|
||||
VIP_CACHE['ts'] = 0.0
|
||||
GLOBAL_MUSIC_U_CACHE['v'] = None
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""管理后台(/admin/site/*)端点(FastAPI 栈)——与 Flask 侧逐字段对齐。
|
||||
"""管理后台(/admin/site/*)端点。
|
||||
|
||||
鉴权:与 auth_system.admin_required 同口径(403 + error_code='forbidden')。
|
||||
|
||||
说明:这些端点多为对 auth_system 同步函数(SQLite / SMTP / 文件)的薄封装,
|
||||
统一经 asyncio.to_thread 执行以不阻塞事件循环;操作者名取当前管理员用户名
|
||||
(与 Flask _actor 同口径)。
|
||||
统一经 asyncio.to_thread 执行以不阻塞事件循环;操作者名取当前管理员用户名。
|
||||
"""
|
||||
import asyncio
|
||||
import math
|
||||
|
||||
+6
-6
@@ -1,18 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""认证操作端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""认证操作端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST /api/v1/auth/logout ↔ Flask /auth/logout(顶号:递增 login_seq + 删 Cookie)
|
||||
- POST /api/v1/auth/heartbeat ↔ Flask /auth/heartbeat
|
||||
- POST /api/v1/auth/ann_seen ↔ Flask /auth/ann_seen
|
||||
- POST /api/v1/auth/login ↔ Flask /auth/login(密码校验 + 失败锁定 + 管理员 2FA 转票据)
|
||||
|
||||
设计:
|
||||
- 登录失败锁定与管理员 2FA 票据表**复用 web.auth_state 的模块级状态**,两栈共享同一份
|
||||
- 登录失败锁定与管理员 2FA 票据表**复用 web.auth_state 的模块级状态**,全进程共享同一份
|
||||
计数/票据,避免「换个框架打就绕过锁定」。
|
||||
- 密码校验(PBKDF2,10 万次迭代,CPU 密集)与库操作经 asyncio.to_thread 执行,
|
||||
不阻塞事件循环。
|
||||
- 会话 Cookie 由 core.session 签发(与 Flask 同格式同密钥),删 Cookie 亦同口径。
|
||||
- 会话 Cookie 由 core.session 签发(沿用同一格式与密钥,存量会话继续有效),删 Cookie 亦同口径。
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
@@ -52,7 +52,7 @@ def _cookie_kwargs() -> dict:
|
||||
|
||||
|
||||
def _login_response(payload: dict, message: str, uid: int, seq: int) -> JSONResponse:
|
||||
"""构造带会话 Cookie 的成功响应(Cookie 格式与 Flask 一致)。"""
|
||||
"""构造带会话 Cookie 的成功响应(沿用同一 Cookie 格式)。"""
|
||||
resp = JSONResponse(content={'status': 200, 'success': True, 'message': message, 'data': payload})
|
||||
cookie = cs.dumps(deps.secret_key(), {
|
||||
'site_uid': uid, 'site_uid_seq': seq, 'site_uid_ts': int(time.time()),
|
||||
@@ -78,7 +78,7 @@ async def logout(user: Annotated[SiteUser | None, Depends(current_user)]):
|
||||
|
||||
@router.post('/auth/heartbeat')
|
||||
async def heartbeat(user: Annotated[SiteUser | None, Depends(current_user)]):
|
||||
"""会话保活:返回最新次数;超限/失效给 401(expired 语义与 Flask 同)。"""
|
||||
"""会话保活:返回最新次数;超限/失效给 401(expired 语义不变)。"""
|
||||
if user is None:
|
||||
return error('登录已失效', 401, error_code='unauthorized')
|
||||
row = await repo.get_user_by_id(user.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""认证状态端点(FastAPI 栈)——与 Flask /auth/me 响应契约逐字段对齐。
|
||||
"""认证状态端点。
|
||||
|
||||
- GET/POST /api/v1/auth/me ↔ Flask /auth/me
|
||||
· user:站点用户公开字段(未登录为 None)
|
||||
@@ -28,7 +28,7 @@ async def auth_me(
|
||||
user: Annotated[SiteUser | None, Depends(current_user)],
|
||||
request: Request,
|
||||
):
|
||||
"""当前会话与站点公开配置(未登录也返回,与 Flask 同口径)。"""
|
||||
"""当前会话与站点公开配置(未登录也返回)。"""
|
||||
await ratelimit.guard(request, bucket='free', limit=30, window=60)
|
||||
try:
|
||||
ann = await repo.get_announcement()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""结果交付端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""结果交付端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST/GET /api/v1/download/result ↔ Flask /download/result(单曲)
|
||||
- POST/GET /api/v1/download/batch_zip/result ↔ Flask /download/batch_zip/result(批量 ZIP)
|
||||
|
||||
语义对齐要点:
|
||||
- 任务归属:仅任务本人的站点账号可领取(非本人 403)
|
||||
- 状态语义:进行中 202 / 暂停 200+提示 / 取消 200+原因 / 失败 500+原因 /
|
||||
文件缺失 404(与 Flask 分支逐一对应)
|
||||
文件缺失 404(与历史分支逐一对应)
|
||||
- 交付物:FileResponse 流式发送;Cache-Control: no-store;
|
||||
X-Download-Filename(URL 编码,供前端取回原始文件名)
|
||||
- 文件清理仍由监控线程按 35 分钟保留期统一回收(交付请求不删文件)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""下载提交端点(FastAPI 栈)——与 Flask 侧逐字段对齐。
|
||||
"""下载提交端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST /api/v1/download ↔ Flask /download(计费 1 次;file/json 两模式)
|
||||
- POST /api/v1/download/batch_zip ↔ Flask /download/batch_zip(按去重歌曲数计费)
|
||||
- POST /api/v1/podcast/download ↔ Flask /podcast/download(计费 1 次)
|
||||
- POST /api/v1/podcast/download/batch_zip ↔ Flask /podcast/download/batch_zip
|
||||
|
||||
计费:经 web.billing.require_billing(预扣+开账同事务 / 失败只退 open 行),
|
||||
任务登记与启动复用 web.tasks_registry(与 main._register_download_task 同口径);
|
||||
worker 仍复用 main 的既有实现(无 flask 依赖),故账本与交付语义不变。
|
||||
任务登记与启动由 web.tasks_registry 承担;
|
||||
worker 复用 services 层既有实现,账本与交付语义不变。
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
@@ -283,7 +283,7 @@ async def download_batch_zip(user: Annotated[SiteUser | None, Depends(current_us
|
||||
# ============ 播客单集下载 ============
|
||||
|
||||
def _podcast_billing_count(data: dict[str, Any]) -> int:
|
||||
"""播客打包计费(与 Flask _podcast_billing_count 同口径)。"""
|
||||
"""播客打包计费。"""
|
||||
if data.get('radio_id'):
|
||||
return 1
|
||||
limit = _safe_int(data.get('limit', 200) or 200, 200)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""首页与静态资源(FastAPI 栈)。
|
||||
"""首页与静态资源。
|
||||
|
||||
- GET / 首页:渲染同一份 templates/index.html,禁止缓存(与 Flask 一致)
|
||||
- GET / 首页:渲染 templates/index.html,禁止缓存
|
||||
- 静态资源由 Starlette StaticFiles 挂载 /static(见 web/app.py)
|
||||
|
||||
说明:首页在 Flask fallback 模式下由 Flask 兜底提供;本端点用于「关闭 Flask 后」
|
||||
FastAPI 独立提供首页(迁移完成的形态)。两者渲染的是同一模板,故内容一致。
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
@@ -21,8 +18,8 @@ _TEMPLATE = Path(__file__).resolve().parent.parent.parent / 'templates' / 'index
|
||||
async def index(request: Request) -> HTMLResponse:
|
||||
"""首页(禁止缓存,确保前端更新即时生效)。
|
||||
|
||||
用 Jinja2 以 Flask 相同的默认口径渲染(keep_trailing_newline=False),
|
||||
使输出与 Flask render_template 逐字节一致。
|
||||
用 Jinja2 以与历史模板渲染一致的默认口径(keep_trailing_newline=False),
|
||||
使输出逐字节不变。
|
||||
"""
|
||||
import jinja2
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""音乐端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""音乐端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- 免费解析类(需登录,不扣次数):
|
||||
POST/GET /api/v1/toplist ↔ Flask /toplist
|
||||
POST/GET /api/v1/artist_songs ↔ Flask /artist_songs
|
||||
@@ -11,7 +11,7 @@
|
||||
POST/GET /api/v1/playlist ↔ Flask /playlist
|
||||
POST/GET /api/v1/song ↔ Flask /song(四种 type:url/name/lyric/json)
|
||||
|
||||
参数获取沿用 Flask 口径:GET query / POST json / POST form 三路兼容。
|
||||
参数获取口径:GET query / POST json / POST form 三路兼容。
|
||||
上游调用走 music_api 的**异步**方法(原生并发,无同步桥)。
|
||||
"""
|
||||
import asyncio
|
||||
@@ -62,7 +62,7 @@ def _nick_lock() -> asyncio.Lock:
|
||||
|
||||
|
||||
async def mask_server_nickname(nick: str, user: SiteUser) -> str:
|
||||
"""非管理员响应中把服务器账号昵称替换为站点名(与 Flask _mask_server_nickname 同口径)。
|
||||
"""非管理员响应中把服务器账号昵称替换为站点名。
|
||||
|
||||
- 管理员本人可见真实昵称;
|
||||
- 缓存 10 分钟内复用;上游拿不到时回落站点库里的最后已知昵称继续遮蔽。
|
||||
@@ -175,7 +175,7 @@ async def search(user: LoginUser, request: Request):
|
||||
except APIException:
|
||||
return error('服务暂时不可用,请稍后再试', 500)
|
||||
|
||||
# 向后兼容字段 + 服务器昵称遮蔽(与 Flask 侧同口径)
|
||||
# 向后兼容字段 + 服务器昵称遮蔽
|
||||
if result:
|
||||
for song in result:
|
||||
if song.get('type', 'song') == 'song' and 'artists' in song:
|
||||
@@ -191,7 +191,7 @@ async def search(user: LoginUser, request: Request):
|
||||
# ============ 个人/全局账号 Cookie ============
|
||||
|
||||
def _global_music_u() -> str:
|
||||
"""全局账号 MUSIC_U(5 分钟缓存,与 Flask main._global_music_u 同源同缓存)。"""
|
||||
"""全局账号 MUSIC_U(5 分钟缓存)。"""
|
||||
now = time.time()
|
||||
if GLOBAL_MUSIC_U_CACHE['v'] is None or now - GLOBAL_MUSIC_U_CACHE['ts'] > 300:
|
||||
try:
|
||||
@@ -209,7 +209,7 @@ async def _resolve_user_cookies(
|
||||
"""解析个人账号 Cookie(浏览器传入的 user_cookie,不落服务器)。
|
||||
|
||||
全局账号(cookie.txt)零个人身份权限:浏览器提交的 MUSIC_U 若与全局账号一致
|
||||
(全局 Cookie 泄露被当个人 Cookie 冒用),一律拒绝并记审计(与 Flask 同口径)。
|
||||
(全局 Cookie 泄露被当个人 Cookie 冒用),一律拒绝并记审计。
|
||||
"""
|
||||
ck = media.parse_cookie_str(data.get('user_cookie'))
|
||||
mu = (ck.get('MUSIC_U') or '').strip()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""播客端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""播客端点。
|
||||
|
||||
已迁移(免费解析类,需登录,不扣次数):
|
||||
端点(免费解析类,需登录,不扣次数):
|
||||
- POST/GET /api/v1/podcast/parse ↔ Flask /podcast/parse
|
||||
- POST/GET /api/v1/podcast/programs ↔ Flask /podcast/programs
|
||||
- POST/GET /api/v1/podcast/search ↔ Flask /podcast/search
|
||||
- POST/GET /api/v1/podcast/hot ↔ Flask /podcast/hot
|
||||
|
||||
说明:podcast_api 仍是同步模块(requests),此处以 asyncio.to_thread 包装,
|
||||
让阻塞的上游调用不占用事件循环——行为与 Flask 侧一致,且不阻塞其它请求。
|
||||
让阻塞的上游调用不占用事件循环——行为不变,且不阻塞其它请求。
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
@@ -164,6 +164,6 @@ async def podcast_hot(_user: LoginUser, request: Request):
|
||||
categories = await asyncio.to_thread(podcast_api.get_categories)
|
||||
except APIException:
|
||||
categories = []
|
||||
# 满页视为可能还有更多(与 Flask 同口径)
|
||||
# 满页视为可能还有更多
|
||||
return success({'radios': radios, 'categories': categories,
|
||||
'has_more': len(radios) >= limit}, '获取热门播客成功')
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""注册 / 找回密码 / 卡密 / 验证码(FastAPI 栈)——与 Flask 侧响应契约逐字段对齐。
|
||||
"""注册 / 找回密码 / 卡密 / 验证码。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST /api/v1/auth/send_code ↔ Flask /auth/send_code
|
||||
- POST /api/v1/auth/register ↔ Flask /auth/register
|
||||
- POST /api/v1/auth/trial ↔ Flask /auth/trial
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
说明:
|
||||
- 同步的 auth_system 调用(SQLite 事务、邮件 SMTP、PBKDF2)经 asyncio.to_thread 执行,
|
||||
不阻塞事件循环;行为与 Flask 完全一致。
|
||||
不阻塞事件循环;行为与历史实现完全一致。
|
||||
- 发码 IP 限流复用 web.auth_state 的模块级状态 `_SEND_CODE_IP`。
|
||||
- 失败锁定/验证码消费等安全口径逐字保留。
|
||||
"""
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""站点数据端点(FastAPI 栈):走 core.db 异步 SQLite(读并发/写串行/WAL)。
|
||||
"""站点数据端点:走 core.db 异步 SQLite(读并发/写串行/WAL)。
|
||||
|
||||
这些是**新增**的 `/api/v1` 只读端点,用于验证异步读写路径与共享同一份站点库;
|
||||
不替代、不影响 Flask 侧既有端点。鉴权端点(需登录/管理员)待会话互通层就绪后再迁移。
|
||||
这些是 `/api/v1` 下的只读端点,走异步读写路径并共享同一份站点库;
|
||||
鉴权端点(需登录/管理员)由 web.deps 统一把关。
|
||||
|
||||
注意:本模块只做读(与 announcements/settings 这些低风险资源对应),
|
||||
写操作仍归 auth_system 的事务口径,避免迁移期两套写路径互相踩踏。
|
||||
写操作统一归 auth_system 的事务口径,避免两套写路径互相踩踏。
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""音频流代理(FastAPI 栈)——与 Flask /audio_stream 行为逐项对齐。
|
||||
"""音频流代理。
|
||||
|
||||
要点(对齐 Flask 实现):
|
||||
- 签名校验:token 由 core.security 校验(密钥同源,两栈 token 互认);失效 403。
|
||||
要点:
|
||||
- 签名校验:token 由 core.security 校验(密钥同源);失效 403。
|
||||
- HEAD 无正文:直接 200 + Accept-Ranges,不请求上游。
|
||||
- Range 透传:客户端 Range 原样转发上游,206/Content-Range 原样回传(拖动进度可用)。
|
||||
- 编码透明:上游若带 Content-Encoding(aiohttp 会解压),不得沿用压缩前的
|
||||
Content-Length/Content-Range(否则头体不一致,同 Flask F92)。
|
||||
Content-Length/Content-Range(否则头体不一致)。
|
||||
- 流式转发:16KB 分块,不整段入内存;下游中断时 finally 关闭上游响应。
|
||||
- Cache-Control: no-store。
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""系统端点(迁移样板):/health、/api/info。
|
||||
"""系统端点:/health、/api/info。
|
||||
|
||||
/flask 侧的这两个端点无鉴权、无状态、无任务依赖,是最安全的迁移起点。
|
||||
此处实现与 main.py 的 health_check / api_info **响应契约逐字段对齐**,
|
||||
用于验证双框架一致性(见 .zcode/refactor/parity.py)。
|
||||
两个端点无鉴权、无状态、无任务依赖。
|
||||
响应契约与历史快照逐字段对齐,
|
||||
由 tests/golden/responses.json 兜住回归。
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -16,7 +16,7 @@ from web.response import error, success
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 与 main.py api_info 的 endpoints 表保持一致(迁移期以 Flask 侧为准,逐字对齐)
|
||||
# 与 api_info 的 endpoints 表保持一致(逐字对齐历史契约)
|
||||
_ENDPOINTS = {
|
||||
'/health': 'GET - 健康检查',
|
||||
'/song': 'GET/POST - 获取歌曲信息',
|
||||
@@ -51,7 +51,7 @@ def health_check():
|
||||
'timestamp': int(time.time()) if 'time' in sys.modules else None,
|
||||
'cookie_status': cookie_status,
|
||||
'cookie_count': len(cookies),
|
||||
'downloads_dir': 'configured', # R5-P3:不向匿名访客回显服务器绝对路径
|
||||
'downloads_dir': 'configured', # 不向匿名访客回显服务器绝对路径
|
||||
'version': APP_VERSION,
|
||||
}
|
||||
return success(health_info, "API服务运行正常")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""任务状态端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
|
||||
"""任务状态端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST/GET /api/v1/topen ↔ Flask /api/topen(请求统计,无鉴权)
|
||||
- POST/GET /api/v1/queue ↔ Flask /api/queue(排队状态,无鉴权)
|
||||
- POST/GET /api/v1/batch_progress ↔ Flask /api/batch_progress(需登录,仅本人任务)
|
||||
@@ -9,8 +9,8 @@
|
||||
- POST/GET /api/v1/task/heartbeat ↔ Flask /api/task/heartbeat(需登录)
|
||||
- POST/GET /api/v1/task/list ↔ Flask /api/task/list(需登录)
|
||||
|
||||
这些端点读写 core.state 的共享任务表(与 Flask 同一批对象),因此两栈在迁移期
|
||||
看到的是同一份任务状态——这是「单进程双框架」能成立的关键。
|
||||
这些端点读写 core.state 的共享任务表(与任务执行侧同一批对象),因此
|
||||
看到的是同一份任务状态。
|
||||
"""
|
||||
import time
|
||||
from typing import Annotated, Any
|
||||
@@ -41,7 +41,7 @@ LoginUser = Annotated[SiteUser, Depends(require_login)]
|
||||
|
||||
|
||||
def _touch_device(device_id: str) -> None:
|
||||
"""记录设备心跳(与 Flask _touch_device 同口径,共享 DEVICE_LAST_SEEN)。"""
|
||||
"""记录设备心跳(与 core.state 的 DEVICE_LAST_SEEN 共享)。"""
|
||||
device_id = str(device_id or '')[:64]
|
||||
if not device_id:
|
||||
return
|
||||
@@ -57,7 +57,7 @@ def _owned_by(task: dict[str, Any], user: SiteUser) -> bool:
|
||||
|
||||
|
||||
def _semaphores():
|
||||
"""批量打包/下载信号量(Flask 侧对象,懒取以共享同一计数)。"""
|
||||
"""批量打包/下载信号量(懒取,与 core.state 中的同一对象共享计数)。"""
|
||||
import services.runtime as services
|
||||
return services.BATCH_ZIP_SEM, services.api_service.downloader._download_semaphore
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""个人账号与公开播放端点(FastAPI 栈)——与 Flask 侧响应契约逐字段对齐。
|
||||
"""个人账号与公开播放端点。
|
||||
|
||||
已迁移:
|
||||
端点:
|
||||
- POST /api/v1/user/qr/create ↔ Flask /user/qr/create(扫码登录二维码 SVG)
|
||||
- POST /api/v1/user/qr/check ↔ Flask /user/qr/check(轮询登录状态)
|
||||
- POST /api/v1/user/status ↔ Flask /user/status
|
||||
|
||||
Reference in new issue
Block a user