diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index fd96d89..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -retro/acceptance-log.jsonl merge=union diff --git a/docker/deploy_targets.json b/docker/deploy_targets.json deleted file mode 100644 index a69fa7d..0000000 --- a/docker/deploy_targets.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "targets": { - "fordeal": { - "dir": "/home/lixing/docker/media-resolver-api" - } - } -} diff --git a/docker/push_to_acr.sh b/docker/push_to_acr.sh index 4314b96..a9f2473 100755 --- a/docker/push_to_acr.sh +++ b/docker/push_to_acr.sh @@ -8,20 +8,10 @@ ACR_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/${IMAGE_NAME}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" RELEASE_TAG="$(git -C "${PROJECT_ROOT}" rev-parse --verify HEAD)" -DEPLOY_TARGET="${DEPLOY_TARGET:-fordeal}" -DEPLOY_DIR="$(python3 - "${PROJECT_ROOT}/docker/deploy_targets.json" "${DEPLOY_TARGET}" <<'PY' -import json -import sys - -with open(sys.argv[1]) as f: - target = json.load(f)["targets"][sys.argv[2]] -print(target["dir"]) -PY -)" +DEPLOY_DIR="${MEDIARESOLVERAPI_DEPLOY_DIR:?MEDIARESOLVERAPI_DEPLOY_DIR must be set to the deploy directory on the target host}" docker info >/dev/null grep -q "${ACR_REGISTRY}" "${HOME}/.docker/config.json" -test -n "${DEPLOY_DIR}" if [ -n "$(git -C "${PROJECT_ROOT}" status --porcelain)" ]; then echo "[ERROR] Refusing to publish a dirty worktree under ${RELEASE_TAG}" >&2 exit 1 @@ -41,4 +31,4 @@ docker build \ docker tag "${IMAGE_NAME}:latest" "${ACR_IMAGE}:${RELEASE_TAG}" docker push "${ACR_IMAGE}:${RELEASE_TAG}" -echo "Published ${ACR_IMAGE}:${RELEASE_TAG} for ${DEPLOY_TARGET}:${DEPLOY_DIR}" \ No newline at end of file +echo "Published ${ACR_IMAGE}:${RELEASE_TAG} for ${DEPLOY_DIR}" \ No newline at end of file diff --git a/docs/handoff-generic-fallback-engine.md b/docs/handoff-generic-fallback-engine.md index d9b388b..c5ddd0b 100644 --- a/docs/handoff-generic-fallback-engine.md +++ b/docs/handoff-generic-fallback-engine.md @@ -206,15 +206,15 @@ curl -s -G 'https://api.tikhub.io/api/v1/kuaishou/web/fetch_one_video' \ ## 7. 部署与生产验证 -部署走 `docker-deploy` skill(先 build)。目标见 `docker/deploy_targets.json`: -`fordeal:/home/lixing/docker/media-resolver-api`。 +部署走 `docker-deploy` skill(先 build)。目标部署目录由环境变量 `MEDIARESOLVERAPI_DEPLOY_DIR` +提供(缺失即报错;真实路径不入仓): ```bash # build + push ACR -cd docker && bash push_to_acr.sh +cd docker && MEDIARESOLVERAPI_DEPLOY_DIR= bash push_to_acr.sh # 服务器拉取重启 -scp docker/pull_and_deploy.sh fordeal:/home/lixing/docker/media-resolver-api/docker/ -ssh fordeal "cd /home/lixing/docker/media-resolver-api && bash docker/pull_and_deploy.sh" +scp docker/pull_and_deploy.sh fordeal:/docker/ +ssh fordeal "cd && bash docker/pull_and_deploy.sh" ``` **坑位:** @@ -234,7 +234,7 @@ ssh fordeal "cd /home/lixing/docker/media-resolver-api && bash docker/pull_and_d ## 8. 环境与命令 - Python 解释器:**`.venv/bin/python`**(项目根 `.venv`)。跑测试:`.venv/bin/python -m pytest -q`。 -- 项目根:`/home/zlx/projects/work/MediaResolverAPI`。 +- 项目根:`/path/to/MediaResolverAPI`。 - 当前测试基线:**97 passed**(重构后不得减少)。 - 大量命令/日志输出优先用 context-mode 的 `ctx_batch_execute`(避免污染上下文)。 diff --git a/docs/sessions/260922-memory-placement/progress/cardD2-mediaresolverapi-progress.md b/docs/sessions/260922-memory-placement/progress/cardD2-mediaresolverapi-progress.md deleted file mode 100644 index f33af6e..0000000 --- a/docs/sessions/260922-memory-placement/progress/cardD2-mediaresolverapi-progress.md +++ /dev/null @@ -1,22 +0,0 @@ -# MediaResolverAPI memory placement progress - -## 里程碑 1:原样迁入条目 - -- 当前阶段:实现 -- 本段结论:已将 agent-config 主 clone 中确认属于 MediaResolverAPI 的 `fordeal-slow-link-e2e.md` 原样复制到本仓 `memory/`。源文件为 1300 字节,迁入后待用 sha256 验证。 -- 关键决策与已否决方案:遵守逐字节搬运,不修改正文或 frontmatter;不复制工具、不新增 README、不新增迁移字段。 -- 下一步唯一动作:使用 agent-config 主 clone 的 `build_index.py` 生成本仓 `memory/INDEX.md`。 - -## 里程碑 2:索引派生与约束验收 - -- 当前阶段:实现 -- 本段结论:已由 agent-config 主 clone 的 `build_index.py` 生成 `memory/INDEX.md`,索引收录 1 条 fact;`--check`、哈希、条目数、工具未入库及 pickup 条件均通过。 -- 关键决策与已否决方案:生成器按 worktree 路径调用时会把 worktree 写入重建命令,违反悬空路径硬判据;未手改索引,改用本仓主 checkout 的稳定路径做一次临时真身生成,复制结果后清理临时文件,最终索引不指向 worktree 或 runtime release。 -- 下一步唯一动作:运行 `make test`,完成最终验收并提交索引与本进度段。 - -## 里程碑 3:验证完成 - -- 当前阶段:实现完成,待最终提交取证 -- 本段结论:`make test` 通过,413 个测试全部通过,产生 200 个既有弃用/依赖告警;本卡没有触碰业务代码、测试、CI、Makefile 或环境文件。 -- 关键决策与已否决方案:保留测试告警原样,不为本卡范围外问题扩展修改;只提交本卡允许路径。 -- 下一步唯一动作:提交索引与进度文件,并记录最终提交、统计和干净状态。 diff --git a/docs/sessions/resolve-intent/design.md b/docs/sessions/resolve-intent/design.md deleted file mode 100644 index 3cce6ec..0000000 --- a/docs/sessions/resolve-intent/design.md +++ /dev/null @@ -1,28 +0,0 @@ -# DESIGN-note:resolve 请求意图(只下音频 / 清晰度封顶) - -下游(如 ASR)只需要音频时,不该被迫下载完整 MP4;调用方也应能封顶清晰度以控制体积与耗时。本 note 覆盖 issue #23 的完整落地计划,分两卡串行:卡 1 落地 `download_mode`,卡 2 落地 `quality` 封顶。 - -不做:`variants[]` 菜单式响应(当前无第二个真实消费者;意图式落地后它只是中间产物的序列化,届时再加);转码与容器格式协商(不承诺 MP3,ASR 消费的是可解码音频流,m4a/opus 均可);yt-dlp;改变任何默认行为;把「最高清」立为全站新不变式。 - -选定方案: - -1. 请求体加 `download_mode: "video" | "audio"`(缺省 `video`)与 `quality: "p"`(可选)。参数表达意图,不表达容器格式。 -2. 响应加 `media_type: "video" | "audio"`(缺省 `video`)自描述字段;直链仍放 `video_url`,向后兼容。 -3. `audio` 意图的三层语义(按平台静态路由,不逐个试): - - YouTube:TikHub 响应里选纯音频轨(v2 schema 的 `streamingData.adaptiveFormats` 中 `mimeType` 以 `audio/` 开头者;现状代码在 `app/services/platforms/youtube.py:136-139` 把 adaptiveFormats 整体丢弃,本卡捡起音频部分)。 - - twitter / tiktok / instagram / pinterest / facebook:直接走 Cobalt,请求体带 `downloadMode: "audio"`(上游原生支持,本仓此前未透传);audio 模式不打 TikHub。 - - douyin / kuaishou / xiaohongshu / wechat_channels:无音频路径,显式报错——HTTP 400,body 含稳定机读前缀 `audio_not_available`,且不发起任何 provider 调用。禁止静默退回视频。 -4. `quality` 语义是「封顶」不是「精确匹配」:≤ cap 取最高档;全部高于 cap 时取最小超档。显式 `quality` 覆盖平台默认规则;不传时平台默认原样(Twitter 仍封 1080p,YouTube 仍优先 1080p 压 4K)。与 Twitter 已合并的 1080p 封顶同一语义。 -5. 缓存键从 `(platform, video_id)` 扩为含 `download_mode`(卡 1)与归一化 `quality`(卡 2)的元组;存量缓存行视为 `video`/默认档继续服务。同一 `video_id` 的不同意图结果互不覆盖——这是本功能唯一的 P1 级静默出错风险。 - -不采用:菜单式 `variants[]`(无第二消费者);`format=mp3`(引入转码责任);audio 不支持时静默退回视频(违反 fail-fast);顺手统一各平台默认选流规则(非目标,默认行为逐字节不变)。 - -不变式(均须测试锁死;卡 1 锁 1/2/3/5,卡 2 锁 4): - -1. [实测] 不传新参数的请求行为逐字节不变:既有测试文件零改动且全量绿。 -2. [实测] audio 路由矩阵:youtube → TikHub 纯音频轨;twitter/tiktok/instagram/pinterest/facebook → 只走 Cobalt 且请求体含 `downloadMode: "audio"`;douyin/kuaishou/xiaohongshu/wechat_channels → HTTP 400 含 `audio_not_available` 且零 provider 调用。 -3. [实测] 同一 `(platform, video_id)` 的 audio 与 video 缓存互不覆盖;旧表结构迁移后存量行继续命中 video 模式。 -4. [实测] quality 封顶:≤cap 最高档;全 >cap 取最小超档;显式 quality 覆盖平台默认。 -5. [实测] `media_type` 与直链一致:audio 时 `video_url` 是音频流地址;缺省与缓存旧数据读出均为 `video`。 - -入口:`POST /api/resolve`(TestClient,桩掉 provider HTTP)。提交 `{"url": , "download_mode": "audio"}`,预期 200,`data.media_type == "audio"`,`data.video_url` 为音频流地址。 diff --git a/docs/sessions/resolve-intent/progress/audio-mode-progress.md b/docs/sessions/resolve-intent/progress/audio-mode-progress.md deleted file mode 100644 index bace0d5..0000000 --- a/docs/sessions/resolve-intent/progress/audio-mode-progress.md +++ /dev/null @@ -1,41 +0,0 @@ -## 里程碑 1:设计基线 - -- 当前阶段:implementing -- 本段结论:已按任务卡逐字落盘 resolve 请求意图设计。后续实现只覆盖 download_mode/audio,不提前实现 quality。 -- 关键决策与已否决方案:沿用设计中的意图式参数、静态路由、fail-fast 与缓存隔离;不采用 variants、format=mp3、静默回退视频。 -- 下一步唯一动作:为 VideoInfo 与缓存键增加 download_mode/media_type,并实现旧表自动迁移。 - -## 里程碑 2:VideoInfo 与缓存隔离 - -- 当前阶段:implementing -- 本段结论:VideoInfo 默认携带 `media_type=video`,缓存键已扩展为 `(platform, video_id, download_mode)`。SQLite 启动连接和 CacheService 绑定数据库都会自动补列、重建唯一索引,旧行继续按 video 命中。 -- 关键决策与已否决方案:不依赖 SQLAlchemy `create_all` 做存量迁移;不丢弃旧行、不引入手工 SQL 步骤。 -- 下一步唯一动作:实现 YouTube audio 轨解析与 media_type 填充。 - -## 里程碑 3:YouTube 音频选轨 - -- 当前阶段:implementing -- 本段结论:YouTube audio 模式只读取 v2 `streamingData.adaptiveFormats`,过滤 `audio/*` 且必须有直链,按 bitrate 选择最高轨。默认 video 路径仍沿用原有 `videos.items` / `streamingData.formats` 选择逻辑。 -- 关键决策与已否决方案:现有 web fixture 没有音频轨、v2 fixture 只有纯视频 adaptive 轨;因此不从 web schema 猜音频,也不接受只有 `signatureCipher` 的轨。 -- 下一步唯一动作:把 download_mode 穿过 resolver、TikHub/Cobalt adapter 与端点错误映射。 - -## 里程碑 4:provider 路由与 API 契约 - -- 当前阶段:implementing -- 本段结论:resolver 已实现 audio 静态路由与四平台 fail-fast 400,Twitter 等五平台 audio 只调用 Cobalt;YouTube/TikHub 与 Cobalt adapter 会填充 media_type,Cobalt audio 请求体透传 `downloadMode`。 -- 关键决策与已否决方案:默认 video 请求不增加 Cobalt 字段;audio 失败不退回 video;无音频平台在任何 provider 调用前抛出 `audio_not_available`。 -- 下一步唯一动作:新增集中测试 fixture,覆盖端到端 audio、路由、缓存迁移与 422。 - -## 里程碑 5:测试与契约 - -- 当前阶段:verifying -- 本段结论:新增 13 项 audio 专项测试与精简 fixture,覆盖选轨、Cobalt 透传、平台路由、API 入口、缓存隔离、旧表迁移、旧 JSON 默认值和非法参数。README 已记录 download_mode、media_type、路由矩阵与 400 语义。 -- 关键决策与已否决方案:红验将在真修复提交后只注入判据对应的一处生产逻辑;不修改既有测试文件或测试配置。 -- 下一步唯一动作:运行全量测试并完成缓存隔离、YouTube audio 选轨两项红验后收口报告。 - -## 里程碑 6:最终验证 - -- 当前阶段:completed -- 本段结论:全量测试 382 项通过,缓存隔离与 YouTube audio 选轨红验均以 AssertionError 转红后精确还原。分支工作区干净,改动全部位于任务卡允许范围。 -- 关键决策与已否决方案:无新增决策;不部署生产。 -- 下一步唯一动作:由主脑验收 card 分支 HEAD 并合并。 diff --git a/docs/sessions/resolve-intent/progress/quality-cap-progress.md b/docs/sessions/resolve-intent/progress/quality-cap-progress.md deleted file mode 100644 index a838de8..0000000 --- a/docs/sessions/resolve-intent/progress/quality-cap-progress.md +++ /dev/null @@ -1,69 +0,0 @@ -## 里程碑 1:请求模型与组合校验 - -- 当前阶段:implementing -- 本段结论:ResolveRequest 已接受可选 `quality`,仅允许 `^\d+p$`;与 `download_mode=audio` 组合在 Pydantic 请求校验阶段返回 422。 -- 关键决策与已否决方案:保持 quality 的请求值原样用于后续缓存归一化;不在端点函数内静默忽略 audio + quality。 -- 下一步唯一动作:为 VideoCache 增加 quality 列并完成两步 SQLite 启动迁移。 - -## 里程碑 2:缓存质量键与两步迁移 - -- 当前阶段:implementing -- 本段结论:VideoCache 与 CacheService 已将归一化 quality(缺省为空串)纳入读写键;SQLite 启动迁移可从原始旧表经 download_mode 再迁 quality,旧视频行仍命中缺省档。 -- 关键决策与已否决方案:卡 1 之前的旧索引保留历史名称作为过渡兼容索引,另建完整四列唯一索引;卡 1 结构则直接重建原唯一索引为四列,不丢存量行。 -- 下一步唯一动作:为 YouTube 与 Twitter 加入显式 quality 封顶选流并锁住默认路径不变。 - -## 里程碑 3:YouTube 与 Twitter 封顶选流 - -- 当前阶段:implementing -- 本段结论:YouTube/Twitter 显式 quality 已按短边分辨率实现“≤cap 取最高档、全超 cap 取最小超档、同档比码率”;不传 quality 继续走原默认排序,Twitter 显式 `2160p` 可越过默认 `1080p`。 -- 关键决策与已否决方案:全仓采用短边 `min(width, height)` 作为 `p` 档基准;没有可用分辨率元数据时显式 quality 走该平台原默认路径,不猜测 URL 或码率代表分辨率。 -- 下一步唯一动作:把 quality 穿过 resolver/adapter/provider,并实现 Douyin 多档与其余平台 no-op 行为。 - -## 里程碑 4:全链路穿线与平台适配 - -- 当前阶段:implementing -- 本段结论:quality 已从 API 穿过 VideoResolver、TikHubAdapter 到 YouTube/Twitter/Douyin 等现有解析器;Cobalt 将 `720p` 映射为上游接受的 `videoQuality: "720"`。Douyin fixture 的多档分辨率可封顶,TikTok/Kuaishou/Xiaohongshu 当前 fixture 无可选多档,Instagram 通过适配器保持 no-op。 -- 关键决策与已否决方案:不修改 Instagram/Wechat Channels 禁止文件,也不把 Cobalt 的未知实际档位伪填进响应 quality;不传 quality 时 resolver/provider kwargs 仍保持卡 1 的原形状。 -- 下一步唯一动作:补齐 README 契约、执行红验并跑最终全量测试。 - -## 里程碑 5:契约文档与验证准备 - -- 当前阶段:verifying -- 本段结论:README 已记录 quality 格式、封顶/超档语义、短边比较基准、no-op 平台与 audio 组合 422;实现与专项测试已覆盖端点、缓存、解析器和 Cobalt 边界。 -- 关键决策与已否决方案:不新增 variants 菜单或平台共享选流抽象;不改变未传 quality 的默认分支。 -- 下一步唯一动作:完成两条核心断言红验并运行最终全量测试。 - -## 里程碑 6:最终验证 - -- 当前阶段:completed -- 本段结论:两条核心红验均以 AssertionError 转红并精确恢复;最终全量测试首轮通过 412 项,工作区保持干净。 -- 关键决策与已否决方案:无新增决策;不部署生产,不修改禁止范围文件。 -- 下一步唯一动作:对本收尾提交再次运行卡定全量测试并交主脑验收。 - -## 里程碑 7:统一短边比较基准 - -- 当前阶段:verifying -- 本段结论:收口自检发现 YouTube 显式分支原按 height 比较,与全仓短边决策不一致;现已改为有宽高时比较 `min(width, height)`,并用竖屏 720p fixture 锁住行为。 -- 关键决策与已否决方案:不改 README 的短边契约,也不为横屏 fixture 的“看起来没差异”保留错误实现。 -- 下一步唯一动作:提交该修正并再次运行最终全量测试。 - -## 里程碑 8:最终交付 - -- 当前阶段:completed -- 本段结论:短边基准修正已提交;最新 HEAD 的卡定全量测试通过 412 项,所有实现、测试、fixture、文档和进度存档均在允许范围内。 -- 关键决策与已否决方案:无新增决策;不部署生产。 -- 下一步唯一动作:由主脑验收本卡分支并合并。 - -## 修复轮:Twitter 未知分辨率排序 - -- 当前阶段:修复轮,处理已确认的 Twitter quality 选流 finding。 -- 本段结论:提取 `_select_default_variant` 复用默认三池选择;显式 cap 在没有 `≤cap` 已知档时优先未知分辨率档,再退到最小超档。新增 known>cap+unknown 与 known≤cap+unknown 两条回归用例。 -- 关键决策与已否决方案:保持默认分支的原有三池语义;不把未知档伪造为可比较分辨率,也不改变默认 1080p 行为。 -- 下一步唯一动作:补齐 TikTok/Kuaishou warning 与局部变量重命名。 - -## 修复轮:无元数据可观测性与参数遮蔽 - -- 当前阶段:修复轮,处理 TikTok/Kuaishou 可观测性和局部变量命名 finding。 -- 本段结论:显式 quality 无可用分辨率元数据时,TikTok 与 Kuaishou 各增加一条 warning;Douyin、TikTok、Xiaohongshu 的描述档局部变量已改为 `quality_label`,参数语义未改变。 -- 关键决策与已否决方案:仅增加日志和重命名,不改变既有 no-op/fallback 选流,不新增状态、配置或兼容分支。 -- 下一步唯一动作:运行修复轮专项与全量测试,核对允许范围、提交和工作区状态。 diff --git a/docs/sessions/resolve-intent/progress/twitter-variants-progress.md b/docs/sessions/resolve-intent/progress/twitter-variants-progress.md deleted file mode 100644 index ec6163e..0000000 --- a/docs/sessions/resolve-intent/progress/twitter-variants-progress.md +++ /dev/null @@ -1,41 +0,0 @@ -## 2026-09-14 — VideoInfo contract - -- 当前阶段:implementing — VideoInfo + to_dict -- 本段结论:为统一视频信息增加可选 `variants` 字段,并让缓存序列化带出该键;既有构造方不受影响。 -- 关键决策与已否决方案:沿用任务卡的附加可选字段契约,不增加开关或兼容分支。 -- 下一步唯一动作:在 Twitter 解析层从既有 candidates 构造全部 mp4 variants。 - -## 2026-09-14 — Twitter parsing - -- 当前阶段:implementing — Twitter 解析层 -- 本段结论:Twitter 解析现在把 candidates 中的全部 mp4 variant 映射为 url、bitrate、width、height、quality,并按码率升序输出;HLS 仍不进入 candidates,主选流逻辑未变。 -- 关键决策与已否决方案:无 -- 下一步唯一动作:把 variants 接入缓存命中重建路径。 - -## 2026-09-14 — Cache round-trip - -- 当前阶段:implementing — cache 回环 -- 本段结论:缓存重建从序列化数据透传 `variants`,旧缓存行没有该键时自然得到 `None`;新缓存数据可完整回环。 -- 关键决策与已否决方案:沿用任务卡约定,不添加旧行回填或额外兼容分支。 -- 下一步唯一动作:增加 API 响应模型和 `_build_response` 的 variants 映射。 - -## 2026-09-14 — API response contract - -- 当前阶段:implementing — API 模型与映射 -- 本段结论:新增 `VideoVariant` 和可选 `VideoInfoResponse.variants`,响应映射逐条绝对化 URL;`VideoInfo.variants is None` 时响应字段保持 `null`。 -- 关键决策与已否决方案:variants 只作为常驻附加字段,不增加请求参数或平台分支。 -- 下一步唯一动作:补解析层与 API/缓存回环测试,并先执行未改代码红验。 - -## 2026-09-14 — Tests - -- 当前阶段:implementing — 测试 -- 本段结论:新增解析层完整 mp4、排序/非法码率测试,以及 API force-refresh 与缓存命中逐一相等的 X variants 断言;同时锁定非 Twitter 响应为 `null`。 -- 关键决策与已否决方案:无 -- 下一步唯一动作:完成 README 字段表与示例,并执行红验和全量测试。 - -## 2026-09-14 — Acceptance - -- 当前阶段:completed — 交付文档、红验与全量测试 -- 本段结论:README 的成功响应示例补充 `data.variants: null`,字段表说明 X video 的 mp4 variants、bps 单位、码率升序和下游自选低码率语义;两处红验均以断言失败验证约束有效,全量测试 413 passed。 -- 关键决策与已否决方案:无 -- 下一步唯一动作:写入执行器报告并交给主脑验收。 diff --git a/docs/sessions/sph-download/design.md b/docs/sessions/sph-download/design.md deleted file mode 100644 index 3773278..0000000 --- a/docs/sessions/sph-download/design.md +++ /dev/null @@ -1,167 +0,0 @@ -# DESIGN-note:微信视频号接入 —— 元数据解析 + 服务端流式解密代理 - -## 目标 - -让 `POST /api/resolve` 接受微信视频号分享链接(`https://weixin.qq.com/sph/…`),像其他 8 个 -平台一样返回元数据与一条 `video_url`;**客户端按这条 `video_url` 下载到的必须是可直接播放的 -标准 mp4**,不需要客户端自己做任何解密。 - -## 非目标 - -- **不做 mp3 / 音频提取**(本轮已明确排除;将来要做是独立能力,涉及 ffmpeg 与任务队列)。 -- 不做视频号的用户主页、合集、评论、直播、公众号文章等其他 TikHub 视频号接口。 -- 不做浏览器 `