Skip to content

分阶段交付明细 ​

阶段 0 — 地基 ✅ ​

  • monorepo 骨架(backend/ frontend/ mobile/)
  • Go: Gin + GORM + SQLite + slog + 优雅启停
  • 核心 6 模型(User/Site/SiteStat/Downloader/ActivationCode/SystemConfig)+ AutoMigrate
  • JWT + bcrypt + AES-GCM + 好学 VIP 准入骨架 + HMAC 激活码
  • Vue3 + Element Plus + Tailwind + 液态玻璃主题
  • Flutter 工程 + GlassCard 主题
  • Dockerfile(多阶段)+ docker-compose + Makefile + CI 工作流

阶段 1 — 核心引擎 ✅ ​

  • httpclient:cookie jar/重试/UA/Cloudflare 检测/SSRF 全谱防护(单测)
  • core/event:广播+链式事件总线(单测)
  • core/module:注册表+Run 调度原语 Skip/Continue/Stop(单测)
  • chain:ChainBase + RunModule(单测)
  • site:types(ISiteMetadata) + selector(goquery 配置驱动) + nologin + registry(YAML 加载)
  • site/schemas/nexusphp:HTML 抓取 + Location 头 nologin(httptest 单测)
  • cookiecloud:AES-128-CBC 解密 + host 匹配(单测)
  • auth/hxpt_vip:接 site 适配器抓用户页判 VIP + 激活 guard
  • db/oper/site(CRUD+AES-GCM)+ SiteChain + api/v1/site(CRUD/连接测试/搜索/cookie/inspect)

阶段 2 — 下载器 + 自动化 ✅ ​

  • downloader/qb:login(403 重登) + version 感知(5.0 stop/start) + rid 增量同步 SyncState.merge(单测)+ add/pause/resume/delete/stats
  • downloader/tr:RPC 409 session 握手 + torrent-get/add/stop/start/remove(单测)
  • downloader/manager:多实例 + Invalidate 重建
  • scheduler:robfig/cron + Recover
  • automation:stats-sync(真实:FetchUserInfo→写 SiteStat)+ checkin(真实:POST attendance.php)+ cookie-keepalive(真实:认证 GET 刷新 session + Set-Cookie 轮换回写 + 失效告警)+ hr-check(真实:抓 H&R 列表→未达标/临期告警)+ registration(真实:探测 signup→关闭开放转换通知)
  • notification:bark/telegram/webhook 通道 + Manager 并发扇出(单测)
  • api/v1/downloader + api/v1/notification + WebSocket 路由

阶段 3 — 媒体/文件/辅种/备份 ✅ ​

  • media:TMDB(search/multi+详情,httptest 单测)+ MusicBrainz + 豆瓣(frodo /api/v2,微信 UA + 公开 apiKey 免签名,国产影视主源,开箱即用)+ PosterCache + NFO(单测)
  • filemanager:WebDAV(PROPFIND/PUT/GET/MKCOL,DAV 命名空间解析,单测) + Alist(REST) + 多实例 Manager
  • keepseed:名称 token Jaccard + 大小接近度评分(单测)
  • backup:自身 JSON 导出/导入 + MoviePilot 站点导入
  • models 加 PosterCache/CrossSeed/FileStorage
  • api: /media /keepseed /storages /backup

阶段 4 — 收尾 ✅ ​

  • 安全加固:ratelimit(全局+登录组) + CORS(可配置源) + X-API-Key + RequestID + 审计日志(单测)
  • go:embed:前端 dist 内嵌进后端 → 单镜像完整产品
  • CI:ci.yml(三端测试) + docker.yml(多架构镜像) + flutter-release.yml(apk/windows)
  • Makefile:dev-*/test/image/up/embed-preview

后续增强 ✅ ​

  • 站点校准:好学 hxpt.yaml(源码校准:搜索+VIP=APP专属VIP+torrents,单测钉死)+ 财神 cspt.yaml(实测 cspt.top 验证 no_login)+ 20 站点定义 + EnsureDefaults 自动填充
  • 娱乐游戏:好学(九宫格+魔力刮刮乐+勋章合成) + 财神(刮刮乐),真实契约+单测+实测
  • 仪表盘真实数据 + 趋势:DashboardChain 聚合 SiteStat → Dashboard.vue 真实指标卡+站点明细表;新增 GET /dashboard/trend 按天聚合(每站每日 MAX 再跨站 SUM,规避累积量重复累加)→ 移动端 Flutter 4 指标卡 + 近 7 天上传趋势柱状图(fl_chart) + 日环比
  • 签到自动化:CheckinCapable 接口 + nexusphp Checkin(attendance.php) + automation 真实全站签到
  • 保命三件套:cookie 保活(KeepAliveCapable + httpclient.Cookies 读 jar 轮换回写 + 失效 danger 告警)/ H&R 告警(HRCapable + ParseHR/ParseHRDuration/IsHRDangerous,按状态+剩余考核时间判定)/ 注册开放检测(RegistrationWatch 模型 + oper + CRUD API + 关闭→开放转换通知,去重不刷屏)—— 详见 [[保命三件套]]
  • MTeam JSON API 适配器:第二个 schema(schema:mteam),走 api.m-team.cc + x-api-key;Search(/api/torrent/search) + FetchUserInfo(/api/member/profile);chain.credential 按 schema 分发凭据(mteam→apiKey/其余→cookie),不改 Adapter 接口;容错数值解析(int/float/string);馒头开箱即用
  • 移动端搜索→下载:Flutter 搜索页(跨站聚合 /search → 按站分组 GlassCard 列表 → 选下载器推送 /downloaders/:id/torrents),闭合移动端核心 PT 工作流;addTorrent 补全;馒头站 download_url 空时提示「暂不支持直接下载」
  • 实时种子状态(WebSocket 全栈):后端 gorilla/websocket Hub 连接池 + 每 2s 聚合所有下载器种子广播(无连接则跳过;单下载器失败填 error 不中断);GET /ws?token=<jwt> query token 鉴权 + 握手首帧;移动端 web_socket_channel 单例 + 指数退避重连,任务弹窗实时刷新进度/双速率(↓下载 ↑上传)+ 连接指示灯;Torrent 补 dl_rate
  • H&R 实时告警推送:复用 WS Hub——hr-check 发现危险种子时除 bark/TG/webhook 通知外,即时广播 hr-alert 给在线连接(Message.Data 改 any 支持多类型消息);移动端仪表盘首屏红色告警卡(站点/标题/状态/剩余考核时间),打开 app 即见
  • Cookie 失效实时告警:复用 WS Hub——cookie-keepalive 检测到登录态丢失时除 danger 通知外即时广播 cookie-alert;移动端仪表盘首屏红色告警卡(站点 Chip),与 H&R 共同构成保命实时推送闭环
  • 前端 chunk 分包 + element-plus 按需引入:vite manualChunks 分离 element-plus/vue/vendor(index.js 1084KB→5KB,各 vendor 独立缓存);unplugin-auto-import + unplugin-vue-components + ElementPlusResolver 按需引入(CSS 357→135KB 省 222KB;JS 因用到组件覆盖广含 el-table 重组件未明显下降,按需基础设施就位);清理 dead echarts 依赖(src 零引用)
  • 前端 typecheck 全绿:Backup.vue onFile 重构(去 template 内联 TS 类型注解 + 假 Event hack);tsconfig include auto-imports.d.ts/components.d.ts(按需引入配套声明);el-table slot row 加 as 断言(FileManager/Sites/Downloaders,DefaultRow → 具体类型);修 downloader.ts 重复 remove key(→ removeTorrents,原删下载器 remove 被覆盖的潜在 bug);vue-tsc exit 0
  • 发布前体检:后端 go build/vet/test(19 包 FAIL:0)+ 二进制 ptpatronus.exe 46MB ✅;前端 typecheck exit 0 + build ✅(npm audit 2 个 dev 漏洞 esbuild/vite,仅影响开发期 server,生产构建产物不含 esbuild 运行时,vite 5→8 breaking 暂不盲目升);移动端 analyze + test 23/23 ✅,fl_chart 0.69→1.2 major breaking 暂不升(当前稳定);⚠️ 移动端 release apk 打包需在本地有网络/Android SDK 环境跑(sandbox 内 gradle 首次下载受限)
  • 移动端媒体/海报页:Flutter 媒体页(GET /media/search 跨源 TMDB/豆瓣/MusicBrainz → 海报网格卡:来源徽标/标题/类型/年份/评分;Image.network 海报 + 加载失败占位);home_shell 加「媒体」第 5 tab
  • 移动端娱乐页:Flutter 娱乐页(站点下拉选择 → GET /entertainment/:id/games 游戏卡片 → POST /entertainment/:id/play 玩一次 → 成功/失败 + 奖项/数值/余额结果);home_shell 加「娱乐」第 6 tab,移动端功能集完整(6 tab:仪表盘/搜索/站点/下载器/媒体/娱乐)
  • 上线前安全审查 + 批次 A 加固:6 域并行审查(auth/JWT、crypto、凭据存储、SSRF/出站、注入/路径、WS/CORS/前端)→ 主控逐条源码复核确认。修复上线阻断级 1 + High 4:
    • ① SECRET_KEY fail-fast(config.validateSecrets):release 模式 SECRET_KEY/ACTIVATION_SECRET 为空或等于公开默认值时拒绝启动——根治「忘配即用源码公开 key → 离线伪造管理员 JWT + 解密全库」的免认证接管(docker-compose 默认空值也命中,故原默认部署即中招)。
    • ② SSRF 重定向 + DNS 重绑定(httpclient 自定义 DialContext):每调用按 ssrf 标志解析主机、校验每个 IP 为公网后再直连已校验 IP;首跳与重定向后续跳都走同一拨号路径且复用同一 ctx,一并根除「302→内网/元数据」与「解析后校验、拨号时再解析」两类绕过;保留 ssrf=false 的内网测试/下载器逃生口。
    • ③ WebSocket CSWSH(WSHandler.checkOrigin):CheckOrigin 收敛为 CORS_ALLOWED_ORIGINS 白名单(原恒 true);非浏览器客户端(curl/移动端,无 Origin 头)仍放行。
    • ④ 限流伪造 + 内存放大(router.SetTrustedProxies + 令牌桶回收):未配代理时不信任任何 XFF(ClientIP 取直连对端),防伪造绕过登录限流;令牌桶空闲 2min 定期回收,防海量伪造 key 撑爆内存。
    • 同步更新 config.example.yaml / docker-compose.yml / 部署指南;新增 config(release 强校验) / httpclient(ssrf dial 守卫) / middleware(桶回收) 回归测试,后端 go build/vet/test 全绿。
    • 剩余 P2/P3(JWT 失效机制、备份加密、安全响应头、AES 密钥 KDF 域分离、统一出站客户端、文件管理器 BasePath、JWT 存储与有效期)列入批次 B,发布后可跟进。
  • 安全加固批次 B(后端自包含子集):挑出低风险、后端闭环、无需跨端/迁移的项先落地:
    • ① 安全响应头(middleware.SecurityHeaders):X-Content-Type-Options:nosniff / X-Frame-Options:SAMEORIGIN / Referrer-Policy:no-referrer / X-XSS-Protection:0;release 模式加 HSTS。CSP 因 SPA + element-plus 含大量内联样式/脚本、需 nonce 化才能上,暂缓(错误的 CSP 会打断 UI)。
    • ② 备份导出响应头:Cache-Control:no-store + nosniff,防浏览器缓存明文凭据文件。
    • ③ URL scheme 白名单(oper.validateURLScheme):FileStorage / Downloader 的 URL 写库前校验仅允许 http/https,拒绝 file://、gopher://、ftp://、javascript:——同时覆盖 API 配置与备份导入两条写入路径。
    • ④ 文件管理器 BasePath 真正生效(filemanager.resolvePath):WebDAV/Alist 构造器注入 base_path,每个操作(List/Upload/Download/Delete/Mkdir)入口把用户路径 path.Clean 并约束在 base 之下,防远端 ../ 越界(之前 BasePath 字段持久化了却从不生效);空 base 也消除路径逃逸。
    • 补 security_headers / validate / resolvePath 单测;后端 build/vet/test 全绿。
    • 剩余需跨端 / 数据迁移 / 产品决策的项列入批次 C(待定):JWT 失效+登出+改密(需 DB 字段+前端)、WS 一次性 ticket(需前端+移动端)、refresh token(缩短 JWT 有效期的配套)、AES 密钥 KDF 域分离(会致旧密文无法解密,需迁移脚本)、备份导出 re-auth(需前端密码输入)。
  • 媒体搜索三连修复(只显音乐/无海报/点击无效):实测定位三处独立根因并逐一修复——
    • ① 只显音乐:豆瓣 frodo /search 2024 改版,结果从顶层 items → subjects.items、类型字段 type → target_type,旧解析器读旧结构致豆瓣返回 0 条;叠加 TMDB 默认无 key 跳过 → 只剩 MusicBrainz(全音乐)。douban.go 适配新结构(subjects.items+target_type)并保留旧格式兼容;card_subtitle 作 overview;补 canned-frodo 单测。
    • ② 无海报:豆瓣 doubanio.com 防盗链,浏览器 <img> 直连只回 1×1 占位图(img9 返 418、qnmob3 返 12B 占位)。新增 GET /media/image?url= 反代(白名单 *.doubanio.com/image.tmdb.org + 注入 movie.douban.com Referer + 5MB 上限,SSRF 收敛);豆瓣 cover_url 规整成 m_ratio_poster(2:3 竖版,原 large 是 h/120 横裁);前端 MediaPoster.vue 对豆瓣图走 axios+JWT 取 blob 喂 <img>(img 不带 token 无法过鉴权),其余 CDN 直连,失败/无图回退占位(音乐 🎵 / 影视 🎬)。
    • ②ᵇ JA3 指纹封锁(e2e 实测发现):反代用标准 net/http 取豆瓣图时拿到 HTTP 200 + text/html(反爬 JS 挑战页),同机 curl/wget 却能取真图——豆瓣图片 CDN 按 TLS 指纹(JA3)封 Go(H2/H1、换 UA 都无效,纯指纹问题)。解法:引入 refraction-networking/utls,反代客户端 media_fetch.go 用 HelloChrome_Auto 伪装 Chrome ClientHello + ALPN 仅 http/1.1;e2e 实测 /media/image 返 102KB 真 JPEG(JFIF 签名)。
    • ③ 点击无效:Media.vue 卡片原本无点击。改为点击 → 各源官方详情页(豆瓣条目 / TMDB / MusicBrainz release-group)新标签打开。
    • ④ 移动端对齐:海报——Flutter media_page.dart 原 Image.network 直连豆瓣海报同样防盗链空白,ApiClient 加 mediaImageUri()(豆瓣图→/media/image 反代 URI)+ authHeaders,_poster() 豆瓣图带 JWT 头经反代加载、其余 CDN 直连;点击——MediaInfo.detailUrl 按源映射详情页(豆瓣 subject / TMDB / MusicBrainz),卡片 GestureDetector → InAppBrowser 应用内打开(复用 flutter_inappwebview,免新依赖)。
    • 验证:新增 media_image_test.go(白名单+SSRF 拒绝)+ TestDoubanPosterURL;后端 build/vet/test + 前端 typecheck/build + 移动端 analyze/test(24/24) 全绿;端到端实测(本地起后端 sqlite + X-API-Key 绕 VIP)/media/search?keyword=霸王别姬 返回豆瓣电影(1993) + 海报 URL,/media/image?url=<doubanio> 返 102KB 真 JPEG(utls 介入后,此前为 502 JS 挑战页)。新依赖 refraction-networking/utls v1.8.2(go.mod 转正)。
  • 可维护性:qb 拆 3 文件 + entertainment helpers 抽出 + 单元/集成测试全覆盖(19 包 FAIL:0)

Web UI 体验迭代(2026-06-26) ​

  • iOS 26 液态玻璃图标:新增可复用组件 frontend/src/components/GlassIcon.vue(圆角 squircle 渐变色块 + 顶部玻璃高光 + 白色描边字形,对标 iOS 26 Liquid Glass),替换导航栏 / 品牌 Logo / 仪表盘 4 指标卡 / 下载器类型徽标的全部 emoji。每个图标独立 from→to 配色,经 unplugin-vue-components 自动注册,构建独立分包 GlassIcon-*.js。
  • 修复「加入站点后仪表盘全 0」:根因是统计仅由 stats-sync 定时任务(默认 */30 * * * *)回填,新站最久等 30 分钟才有数据。新增 POST /dashboard/sync 端点——DashboardChain.SyncAll 注入 siteCh 后立即抓取所有启用站点 UserInfo 写快照并返回最新总览。前端两处触发:① 仪表盘「刷新统计」按钮(Dashboard.vue);② Sites.vue 添加站点成功后后台静默 sync,用户切到仪表盘即见数据。
  • 修复「下载器不能编辑」:后端 PUT/DELETE /downloaders/:id 本就存在,缺的是前端入口。Downloaders.vue 卡片补「编辑 / 删除」按钮,复用同一对话框(editingId 区分新建/编辑,密码留空=不修改),并补「默认」开关、下载器类型改用 GlassIcon 徽标。
  • 验证:前端 vue-tsc exit 0 + npm run build ✅;后端 go build/go vet ./... ✅。

Web UI 体验迭代 · 第二轮(2026-06-26) ​

针对实测反馈的 6 项修复:

  • 图标字形居中:GlassIcon 字形容器改为 position:absolute; inset:0 确定尺寸 + 子 svg 用 width/height:60%(原先百分比挂在自动尺寸的 inline-flex span 上解析不确定,字形偏位/尺寸漂移)。
  • 下载器任务状态中文化:qBittorrent 原始状态码(stalledUP/pausedUP/downloading…)+ Transmission(stopped/seeding…)→ 中文标签 stateLabel,状态列不再显示英文。
  • 下载器卡片按钮溢出:.actions 由 flex 改为 grid 2 列(4 按钮 2×2 排布),删除按钮不再超出边框;按钮 width:100% 填满格子。
  • 娱乐页空白:根因 host 不匹配——站点 base_url 常带 www(www.hxpt.org)而游戏注册用裸域(hxpt.org),Registry.Games/Game 改为后缀匹配(hostMatch:精确相等或子域),www.hxpt.org 站点现能匹配到好学游戏。
  • 好学统计全 0(根因):hxpt.yaml user_info.path 由 userdetails.php(需 ?id=<uid>,无 id 抓到非资料页→选择器全不匹配→上传/下载/魔力/分享率全 0)改为 usercp.php(当前用户个人中心,无需 id,与 cspt.yaml/模板一致)。注:做种 仍缺精确选择器(:contains("做种") 会误匹配「做种时间」),暂留 0 待 /sites/inspect 校准。
  • 站点可编辑:Sites.vue 补「编辑」按钮 + 编辑态(editingId,cookie 留空=不修改),与下载器一致。
  • 验证:前端 vue-tsc+build ✅;后端 go build/vet/test ./...(含 entertainment/site)✅。

Web UI 体验迭代 · 第三轮:仪表盘数据根治 + 站点明细字段补全(2026-06-26) ​

针对「仪表盘上传/下载/魔力全 0、站点明细字段太少」的根治性修复(对照 D:\html 三站真实页面 + 参考 MoviePilot/PT-depiler/旧版 PTPatronus 的字段提取范式)。

  • 根因:user_info.fields 选择器错配——三站 usercp.php 根本不是 td.rowhead 表格结构(好学=cute-top-profile、财神=Audiences menu-base-info、蟹黄堡=经典 info_block),旧选择器全部落空;蟹黄堡 yaml 是空壳。统一锚点:NexusPHP 框架常量 font.color_*(uploaded/downloaded/ratio/active/bonus)三站共有,命中后取父节点文本 + 正则抽值即可覆盖三种皮肤。
  • 选择器引擎增强(site/selector.go + types.go):ElementQuery 新增 Selectors []string(回退列表,首个非空胜出)与 Parent bool(命中后爬父节点取文本);Filter.regexp 支持 args 带 :::N 取第 N 捕获组(「当前活动: 27 0」一锚拆做种/下载中)。向后兼容,既有 search/hr 选择器不受影响。
  • 字段链路全程加宽:UserInfo(+leeching/seeding_size/bonus_per_hour) → SiteStat(+level/leeching/seeding_size/bonus_per_hour/join_date) → SiteStatItem + totals → 前端类型 + 表格列。AutoMigrate 自动加列,旧快照不破坏。
  • NexusPHP 多页 best-effort 抓取(site/schemas/nexusphp.go):usercp.php(基础统计+发现 uid) → userdetails.php?id=uid(level/join_date) → mybonus.php(bonus_per_hour) → getusertorrentlistajax?type=seeding(seeding_size,逐 <td> 单元格累加)。任一子页失败不致命。
  • 三站定义对账重写:hxpt.yaml(火花/seeding-2 组拆分)、cspt.yaml(元宝/SVG :has(title:contains) 菜单)、crabpt.yaml(蟹币值/3 个 color_bonus 取首)。defaults.go 的 DefaultNexusPHPUserInfo 升级为 color_* 锚点版 + 默认 detail_fields,其余 ~17 站无 user_info 段自动继承。
  • 修复 ParseBytes:兼容二进制单位 TiB/GiB(去 i 归一)。
  • 前端:明细表新增 等级/做种体积/下载中/时魔/加入时间 列(站点列 fixed 左、横向滚动);新增「总做种体积」卡。
  • 测试钉死真实值:site/testdata/{hxpt,cspt,crabpt}_{usercp,userdetails}.html(截取自真实页面)+ parse_fixture_test.go 断言三站 uploaded/ratio/bonus/seeding/level/join_date 真实数值(好学 104.564TiB/维护开发员、财神 44.069TiB/总版主、蟹黄堡 1.261TiB/蟹币值1621449.1)+ 引擎单测(Selectors 回退/Parent/:::N);schemas/nexusphp_multifetch_test.go httptest 验证多页编排与 404 容错。
  • 验证:后端 go build/vet/test 全绿;前端 vue-tsc+build ✅。
  • 待跟进:① bonus_per_hour/seeding_size 走标准端点 best-effort,部分站(如好学用 getusertorrentlist.php 非 ajax)可能取不到,需 /sites/inspect 实测校准;② 移动端 Flutter dashboard 模型未同步新字段(fromJson 容错不报错,但暂不展示)——✅ 见「移动端:Flutter 仪表盘模型同步新字段」;③ Web UI 全面对齐旧版功能(趋势图/H&R/Cookie 告警卡等移动端已有项)——✅ 见「第四轮」。

Web UI 体验迭代 · 第三轮·续:站点明细操作列 + 发布数 + 侧栏图标居中(2026-06-26) ​

  • 站点明细加列(参考 PT-depiler「我的数据」):发布数 uploads、更新时间 updated_at、操作列。操作列两个图标按钮——用户历史数据(蓝色,弹 el-dialog 展示该站近 90 天历史快照表)+ 刷新数据(绿色,仅重新抓取该站、按钮 loading 态)。
  • 发布数抓取:userdetails「发布种子」是折叠行(计数靠 AJAX),故走 getusertorrentlistajax?type=uploaded——fetchUploadsCount 优先解析响应头 <b>N</b>条记录,回退数种子详情行。best-effort。
  • 新接口:POST /dashboard/sync/:id(单站同步,返回刷新后总览)、GET /dashboard/history/:id?days=N(单站历史快照,倒序最多 500)。链路 DashboardChain.SyncOne + History。
  • 字段链路续扩:UserInfo/SiteStat/SiteStatItem 加 uploads,SiteStatItem 加 updated_at(=SnapAt)。GlassIcon 加 refresh/history 两字形。
  • 侧边栏收起态图标居中:MainLayout.vue 收起时 .brand/.nav-item 改 justify-content:center + 去左右 padding,图标不再左贴边。
  • 测试:nexusphp_multifetch_test.go 增 type=uploaded 路由 + u.Uploads==5 断言。后端 build/vet/test 全绿;前端 vue-tsc+build ✅。

Web UI 体验迭代 · 第三轮·续2:做种体积/时魔按 PT-depiler 校准 + 列顺序(2026-06-26) ​

  • 根因(实测对账 PT-depiler 真值):seedingSize 之前逐行累加体积是错的——NexusPHP 的 getusertorrentlistajax?type=seeding 响应在 table 前有一个汇总 div N | SIZE(见 nexusphp 源码),体积应取该汇总;bonusPerHour 之前用全局「每小时」正则会取到魔力总量等干扰数。
  • 按 PT-depiler NexusPHP.ts 精确方式重写:fetchSeedingSize 优先解析汇总 div「N | SIZE」的体积(正则要求第二段带单位,规避表内 seeders|leechers 误匹配),回退逐 <td> 累加;fetchBonusPerHour 定位 div:contains("你当前每小时能获取")(最深)再用 能获取 锚定取其后数字(避免同元素内魔力总量干扰),多语言/#outer td[rowspan] 回退;fetchUploadsCount 加汇总 div「N」回退。
  • 列顺序:发布数移到做种前面(站点/等级/上传/下载/分享率/发布数/做种/做种体积/…)。
  • 测试:multifetch 用真实 NexusPHP 响应格式(汇总 div + 含干扰数的 mybonus div)钉死 SeedingSize==2GiB、BonusPerHour==123.4(不被 999999 干扰)、Uploads==5。后端全绿;前端 ✅。
  • 部署要点:改动须重建 Docker 镜像(make image && make up)或重跑 ptpatronus.exe 后才生效——运行中的旧镜像/二进制仍是旧行为。

收口「第三轮·待跟进 ③ Web UI 全面对齐旧版」——后端 /dashboard/trend + WS hr-alert/cookie-alert 早齐备、移动端已消费,Web 缺的是趋势图与两张实时告警卡。根因:Web 前端无 WS 客户端,且 api/dashboard.ts 未接趋势端点。

  • 趋势 API(api/dashboard.ts):新增 DashboardTrendPoint 类型 + trend(days=7)(GET /dashboard/trend → data.points),对齐 mobile DashboardTrendPoint。
  • WS 客户端(新建 composables/useWs.ts):模块级单例 + Vue 响应式 refs(connected/hrAlerts/cookieAlerts/torrents),镜像 mobile/lib/core/ws_client.dart——/api/v1/ws?token=<jwt>(URL 由 location.protocol/host 推导,同源),按 type 分发,断线指数退避重连(2…60s 封顶),disconnect() 置 stopped 阻止重连。
  • WS 生命周期(MainLayout.vue):onMounted 连 / onUnmounted 断;登出走 auth.logout() + 跳 /login 卸载外壳 → 自然断开。
  • vite 代理 WS(vite.config.ts):/api 代理补 ws: true(开发期转发 WS 升级;生产同源 go:embed 不受影响)。
  • 仪表盘渲染(Dashboard.vue):load() 并行 overview() + trend(7);渲染顺序对齐移动端——Cookie 失效告警卡(红色,失效站点 chip 列表)→ H&R 告警卡(红色,站点/标题/状态/剩余考核时间,最多 5 条)→ 指标卡墙 → 近 7 天上传趋势(轻量 CSS 柱状图,渐变 #6c5ce7→#4285f4,MM-DD 标签,hover title 显完整字节,空态提示)→ 站点明细;总上传/总下载卡加日环比徽标(末天 vs 前一天 ±%)。
  • 趋势图方案:用纯 CSS 柱状图(flex 等宽列 + 渐变柱 + height%),未重新引入 echarts——契合上轮清理 echarts 死依赖 + chunk 体积优化;7 根柱场景 echarts 能力溢出。
  • GlassIcon:补 alert 字形(Lucide triangle-alert),告警卡 danger 徽标用。
  • 验证:vue-tsc exit 0 + npm run build ✅(新增 useWs 1.03KB、Dashboard 9.45KB,无新 vendor chunk)。
  • 不在本期:WS torrents 已分发但未渲染(下载器实时种子卡属下载器页增强)、WS 连接指示灯、下载量趋势切换、Flutter dashboard 新字段同步(另一 TODO,独立处理)。

移动端:Flutter 仪表盘模型同步新字段 + 站点明细列表(2026-06-26) ​

收口「第三轮·待跟进 ② 移动端 Flutter dashboard 模型未同步新字段(fromJson 容错不报错,但暂不展示)」。

  • 模型同步(mobile/lib/api/dashboard.dart,对齐后端 chain.SiteStatItem/DashboardOverview.totals):
    • SiteStatItem 补 7 字段:level/leeching/seedingSize/uploads/bonusPerHour/joinDate/updatedAt(此前 fromJson 容错丢弃)。
    • DashboardOverview.totals 补 3 字段:leeching/seedingSize/bonusPerHour。
    • DashboardTrendPoint 已与后端 TrendPoint 对齐,不动。
  • 站点明细列表(dashboard_page.dart):新增「站点明细(N)」区——每站一张 GlassCard:站点名 + 等级 chip + 彩色分享率 + 三行统计网格(上传/下载/做种 · 做种体积/发布/下载中 · 魔力/时魔·h)+ 加入/更新时间;空态提示「暂无数据,等待统计同步」。整体 _body 由 Column 改 ListView 可滚动,趋势图由 Expanded 改固定高 190。
  • 测试:dashboard_model_test.dart 补新字段断言(好学 level=维护开发员/seedingSize/uploads/bonusPerHour/joinDate/updatedAt 真值 + 馒头缺字段回落默认值);顺手修 widget_test.dart 陈旧断言 SegmentedButton<bool>→<int>(登录重设计后登录方式 2→3 种,段类型改 int,冒烟测试未跟进——与本期改动无关的历史遗留)。
  • 验证:flutter analyze No issues found! + flutter test 23/23 ✅。

仪表盘趋势图升级:全指标多系列 + 区间选择(2026-06-28) ​

「第四轮」趋势图仅上传单系列 CSS 柱图、无分享率、不可切指标。本轮升级为可切指标/可叠加/带 hover tooltip 的真趋势图,覆盖上传/下载/做种/做种体积/魔力/分享率全指标。

  • 后端(chain/dashboard.go):TrendPoint 补 seeding_size(SQL 内层 MAX + 外层 SUM,与既有累积量防重复累加同构)与 ratio(Scan 后由 uploaded/downloaded 补算,保持 SQL 跨方言稳定;值为当日跨站聚合总分享率)。新增 dashboard_trend_test.go:同日双快照验证「MAX-再-SUM」+ ratio 断言。
  • Web:新建零依赖 TrendChart.vue(viewBox SVG,legend 点按切换/叠加系列,上传=柱、下载/做种/魔力=线、做种体积=面积、分享率走右轴;每列热区 hover tooltip 显当日各指标;复用 utils/format.ts 的 fmtBytes)。Dashboard.vue 替换旧 CSS 柱图为该组件 + 7/30/90 天区间分段(切换仅重拉趋势),删 trendBars computed 与旧柱图 CSS。
  • 方案延续:仍未重新引入 echarts(项目刻意精简依赖,分享图走 Canvas、趋势图走手绘 SVG),延续「第四轮」与 chunk 优化决策。
  • 移动端(dashboard_page.dart):DashboardTrendPoint 补 seedingSize/ratio;趋势卡加 ChoiceChips 指标选择器(6 指标全可选)+ 动态标题「近 7 天 {指标} 趋势」,复用既有 _TrendChart(fl_chart BarChart) 喂选中指标 data。注:fl_chart 单 Y 轴,移动端为「单指标切换」而非 Web 的多系列叠加。
  • 验证:后端 go test ./internal/chain/... ✅;Web vue-tsc + npm run build ✅(Dashboard 18.57KB、vendor 160KB 无 echarts);移动端 flutter analyze No issues found!。

仪表盘趋势图板块重设计:sparkline 内嵌指标卡(2026-06-28) ​

用户反馈独立趋势大卡「不好看且占地方」。改用 sparkline 内嵌指标卡方案:移除独立趋势卡,趋势融进已有指标卡墙,零额外占地。

  • 移除:独立 .trend-card + 全指标多系列 SVG components/dashboard/TrendChart.vue(已删,上方 2026-06-28 段落里的该组件链接随之失效)+ 7/30/90 区间选择器 + trendDays ref/watch。
  • 新增 Sparkline.vue:零依赖迷你折线,viewBox 100×36 + preserveAspectRatio="none" 横向自适应卡片宽度 + vector-effect="non-scaling-stroke" 保 2px 描边不随拉伸变粗,下方渐变面积,HTML 百分比定位列热区 + hover tooltip(日期 + 该指标当日值)。该模式下独立端点圆会变椭圆,故用 area 填充 + round line-cap 替代。
  • 嵌入 Dashboard.vue:5 张指标卡(上传/下载/做种/做种体积/魔力)底部各嵌一条,颜色沿用原 TrendChart SERIES 配色(#6c5ce7/#4285f4/#38ef7d/#43cea2/#f7971e)保持视觉连续。
  • 数据:trend 请求从跟随 trendDays(7/30/90) 改为固定 14 天(sparkline 形态比 7 点丰富);日环比仍取末两天;导出图片 slice(-7) 保持「近 7 天」语义不变。
  • 方案延续:仍未引入 echarts(sparkline 手绘 SVG,延续零依赖画图哲学)。
  • 验证:Web vue-tsc + npm run build ✅(Dashboard 15.79KB)。仅 Web 改动,移动端(fl_chart ChoiceChips 单指标切换)本次未动。

仪表盘导出图片重做:参考 PT-depiler 时间轴海报(2026-06-28) ​

原导出图较简陋(标题 + 5 指标块 + 7 天柱图 + 页脚)。参考 PT-depiler UserDataTimeline(Konva 海报式分享图)重做为「个人 PT 成就海报」,沿用项目零依赖纯 Canvas 2D 哲学(不引 Konva/ECharts/html2canvas)。

  • 重写 dashboardImage.ts:扩展 DashboardShareData(去 trend,加 sites: DashboardShareSite[] + username + totals 对象);exportDashboardImage 改 async,先 Promise.all 并行预载每站 favicon(base_url + /favicon.ico,crossOrigin='anonymous' 防 canvas taint,3s 超时/失败回退),再 Canvas 绘制 5 分区:①深色渐变背景 ②顶部用户名 + 生成时间 ③基础信息(总站点 / PT 龄 / 6 指标两列网格 / 上传量冠亚军 金 #C9B037 银 #B4B4B4)④每站两列明细(等级 + 上传/下载/分享率 + 做种/体积/发布 + 魔力/时魔 + 加入年,共 10+ 字段;favicon 圆 clip,失败 → 站名首字 + 哈希 hue 色块)⑤页脚。布局用单一数据源 layout() 返回各区高度,画布高 = HEADER + 摘要 + 站点区(ceil(N/2)*70) + 页脚 动态算。
  • 改 Dashboard.vue exportImage 改 async:拉 siteApi.list() 按 id join base_url(SiteStatItem 不带),sites 按 uploaded 降序,username 取 useAuthStore().username。
  • 去掉 原「近 7 天上传趋势柱图」(纯成就海报,忠实 PT-depiler)。
  • 关键坑:Canvas drawImage 跨域未授权图会 taint 致 toBlob 抛错——用 crossOrigin='anonymous',无 CORS 头则 onerror 回退色块(不会 taint)。奖牌用纯几何圆 + 序号(无 emoji 字体依赖)。
  • 验证:Web vue-tsc + npm run build ✅(Dashboard 15.79KB)。

仪表盘导出图片配置化扩展:主题/脱敏/背景图/字段开关/7 类冠军(2026-06-28) ​

对照重构前旧版(Kotlin DashboardExporter)补全:导出从「一键直出」升级为配置驱动,弹配置对话框,配置持久化到 localStorage。

  • 新增 dashboardExportConfig.ts:DashboardExportConfig(总览 10 字段开关 / showRank + 7 类 rankMetrics / showSites + 站点白名单 + 数量 / 3 主题 / 双脱敏 / 背景图 + 模糊 + 遮罩)+ loadConfig/saveConfig(localStorage,merge 默认值容错,配额超限返回 false)。
  • 新增 DashboardExportDialog.vue:配置弹窗(el-dialog)。背景图 <input file> → FileReader → canvas 压缩(max 1600 / jpeg 0.85) 再存(控 localStorage 体积);模糊/遮罩滑块;字段/冠军 checkbox;站点白名单 el-select multiple + 数量 el-input-number;脱敏开关。watch deep 自动存盘,超限提示。
  • 重写 dashboardImage.ts:exportDashboardImage(d, config)。主题色系统 themeColors(theme, hasBg)(dark 深紫渐变 / light 浅色 / glass=有背景图→白字 + 暗遮罩);drawBackground 背景图 cover + ctx.filter='blur'(try/catch 降级) + dim 遮罩;脱敏 mask(站点名/用户名→***);总览补 总发布数 / 总时魔·h / 最早入站+站点;冠军扩 7 类(RANK_DEFS 各指标冠军站点,替换旧上传冠亚军);layout 改绝对坐标返回各区 y(总览字段数/冠军数/站点数全动态)。
  • 改 Dashboard.vue:导出按钮 → 打开 dialog(openExport 首次拉 siteApi.list() 缓存 base_url);exportData computed 组装全量 sites(含 site_id,排序/筛选交 config)。
  • 关键坑:① 背景图本地上传(dataURL)同源不 taint;favicon 跨域仍需 crossOrigin。② ctx.filter blur 兼容性包 try/catch。③ localStorage ~5MB → 背景图必压缩。④ EP 实际装 2.14.2,el-radio/checkbox 取值用 value,同时设 label 兼容旧版 a11y。⑤ layout 绝对坐标避免动态字段数下绘制与画布高漂移。
  • 验证:Web vue-tsc + npm run build ✅。

实测娱乐页除九宫格外全失败。对照迁移前 Kotlin 项目(F:\PT-admin\PTPatronus,adapter/lottery/*)逐游戏核对,根因是 Go 重写时游戏契约写错了(非简单缺头):

  • 魔力刮刮乐(invalid character '<'):站点 buy 返回 HTML(内嵌 currentScratchId=N),Kotlin 版用正则抠 id;Go 版误当 JSON 解析 → 炸。
  • 勋章合成(安全验证失败):站点需 csrf_token——GET 页面取 token + 首个 synthesis_id,POST action=draw+params={"synthesis_id":N}+csrf_token 并带 X-CSRF-TOKEN 头,token 失效要刷新重试;Go 版只发 action=draw(无 token)→ 被拦。
  • 财神刮刮乐(401 未授权):财神 API 走 Bearer token,需 POST /api/login(用户名+密码)换 token;Go 版只带 cookie → 401。

修复(按 Kotlin 契约重写):

  • httpclient:新增 PostFormAjaxWith(带额外头,供 csrf X-CSRF-TOKEN)+ GetWithHeaders;既有 PostFormAjax 统一带 X-Requested-With+Referer(具体游戏页 URL,非站点根)。main.go debug 环境改用 debug 日志级别;4 游戏加 logResp(slog.Debug 打站点原始响应 status+body,诊断用)。
  • MagicScratchGame 重写:buy→正则抠 currentScratchId、scratch→JSON、中奖→settle。
  • MedalSynthesisGame 重写:GET 页面取 csrf+synthID→POST draw(X-CSRF-TOKEN)→msg 含「安全验证/csrf/刷新页面」则刷新 token 重试一次;「玩一次」=抽首个合成目标 1 片。
  • 测试:重写 TestMagicScratchGamePlay(HTML buy + settle 校验)、medal_test 三例(正常 / 普通失败不重试 / CSRF 失败重试);go build/vet/test 全绿(22 包 FAIL:0)。
  • 待定 · 财神刮刮乐:需站点用户名+密码(API 登录换 Bearer token)。现 Site 模型只有 cookie,无 username/password。要支持须:Site 加 Username+PasswordEnc 字段(AutoMigrate)+ oper 加解密 + cspt 游戏加 /api/login 换 token 流程 + Web/移动端加站点凭据录入 UI。属跨端功能,待确认是否做。

娱乐:财神刮刮乐功能补全 + 富 UI(对齐原站)(2026-06-26) ​

接上文「待定 · 财神」——落地完整凭据 + 富 UI(用户给出原站 UI 参考:古风金玉主题、奖品展示网格、扇形刮卡、自动刮卡、购卡档位 1/10/50/100、抽奖记录、游戏规则)。

  • 凭据层:Site 模型加 Username + PasswordEnc(AES-GCM,AutoMigrate 自动加列);oper.SiteOper 增 Password() 解密 + SiteInput.Username/Password;entertainment.Creds{Cookie,Username,Password} 贯穿,LotteryGame.Play 签名改为收 Creds(4 游戏全改)。
  • 财神 API 登录:cspt.go 重写——csptLogin(POST /api/login JSON 换 Bearer) + csptRequest(登录→带 token POST→401/未授权重登重试);每次操作重登(游戏单例无法跨站缓存 token)。
  • 能力接口(对齐 Kotlin LotteryCardShop 等):Statable/Purchasable/Prizeable/Historable 可选接口 + 结果类型(StatsResult/BuyOption/PrizeDef/GameRecord);cspt 实现全部(stats=余额/剩余卡、buy=1/10/50/100 档位、prizes=奖品展示、history=抽奖记录)。
  • 新端点:GET /entertainment/:id/games/:game/{stats,buy-options,prizes,history} + POST .../buy;chain 按 resolve() 取站点+游戏+凭据+baseURL 后按能力类型断言分发。
  • Web 富 UI:新增 components/ScratchCardGame.vue——古风金玉主题,含统计栏(剩余刮刮卡/金元宝余额)、奖品展示、刮卡视觉(刮奖区→揭奖动画)、单抽 + 自动刮卡(循环至卡耗尽)、购卡档位(买N送M + 价格)、抽奖记录;Entertainment.vue 改为「站点选择 + 玩法选择 + 按玩法分发」(cspt_scratch 走富组件,其余走通用卡)。
  • 站点表单:Sites.vue + api/site.ts 加「账号/密码」录入(密码 AES-GCM 加密入库,编辑留空=不修改)。
  • 验证:后端 go build/vet/test 22 包全绿(含 cspt 登录/重登/无凭据/重试单测);前端 vue-tsc + build ✅。
  • 待跟进:① 移动端站点表单 + 移动端富娱乐 UI;② 九宫格/魔力刮刮乐/勋章合成的富 UI(当前 Web 走通用卡,可照 ScratchCardGame 范式扩);③ 真机实测各站真实响应(debug 日志已就位:entertainment raw response)。

娱乐:九宫格富 UI(3×3 抽奖动画)(2026-06-26) ​

对照迁移前 Kotlin JggGameUi.kt + 站点源码(jgg/magic_grid_config.php)。

  • 后端:JggGame 实现 Statable(action=get_stats → 今日次数/上限/单次消耗);PlayResult 加 PrizeID;修正 JggGame.Play 奖品在嵌套 prize 对象({id,name,icon,color,type,value})里的解析(此前误读扁平 prize_name, PrizeID 缺失导致动画无法定位),兼容扁平回退。
  • Web 富 UI:新增 components/JggGameUi.vue——固定 9 奖品(id1-8 外圈顺时针、id9 归 VIP1天格);3×3 网格(外圈 8 格 + 中心「开始抽奖」按钮);中奖后高亮顺时针循环减速 3 圈落到中奖格(对齐 Kotlin 节奏:70ms 起、+8/步、封顶 280ms)+ 落定 pop 动画 + 结果展示;统计栏(今日次数/火花余额)。Entertainment.vue 分发:jgg → JggGameUi。
  • 测试:新增 TestJggGamePlayNested(钉死嵌套 prize 的 PrizeID/Prize/Balance 真值);后端 go build/vet/test 22 包全绿;前端 vue-tsc+build ✅。

娱乐:魔力刮刮乐 + 勋章合成富 UI(Web 四玩法齐)(2026-06-26) ​

四个玩法 Web 端均有独立富组件,Entertainment.vue 按 gameKey 分发(通用卡兜底)。

  • 魔力刮刮乐(MagicScratchGame.vue,琥珀/火花主题):后端 MagicScratchGame 实现 Statable(GET magic_scratch.php HTML 抠「当前余额」+「今日剩余 N/M」,对齐 Kotlin loadStats);UI 统计栏(火花余额/今日次数)+ 刮卡视觉(刮奖区→揭奖)+「刮一张」。
  • 勋章合成(MedalSynthesisGame.vue,紫/勋章主题):抽取一片 → 把 Play 返回的碎片汇总(「碎片X×2、碎片Y×1」)拆成碎片卡逐张揭奖动画。注:当前抽取首个合成目标;完整目标选择/碎片库存/合成面板为后续增强(需站点页深度解析)。
  • 验证:后端 22 包全绿;前端 vue-tsc+build ✅。
  • 待跟进:① 移动端富娱乐 UI;② 勋章合成完整面板(目标列表/碎片库存/合成);③ 真机实测各站真实响应校准(debug 日志 entertainment raw response 已就位)。

卡牌合成完整面板 + 真实源码对账(2026-06-26) ​

用户提供好学站点源码备份 F:\www.hxpt.org\public(迁移前 Kotlin 注释即引用此处),据此把卡牌合成(gameKey=medal_synthesis,站点 UI 名「卡牌合成」)做成完整面板并逐行核对契约。

  • 正名:站点 UI 实为「卡牌合成」(Kotlin title="卡牌合成"),后端 MedalSynthesisGame.Name() 由「勋章合成」改为「卡牌合成」。
  • 后端能力(源码 medal_synthesis.php 核对):
    • Synthesizable 接口 + SynthesisTarget/SynthFragment 类型;MedalSynthesisGame 实现 Syntheses/Draw(count 1/10/100/→draw/draw_ten/draw_hundred/draw_custom)/Synthesize。
    • 页面解析移植 Kotlin parseSyntheses(深度配对 <div> 块):synthesis-card/data-synthesis-id/fragment-item/span.current|required/drawFragment(id,count,price)/btn-synthesize/pity-count——正则经源码逐行核对全部对上,TestParseSyntheses 钉死。
    • csrf 对 $_COOKIE['synthesis_csrf_token'](cookie jar 从 GET 带到 POST);Historable:action=get_history params={synthesis_id,page} → data.logs:[{fragment_name,synthesis_name,cost,created_at}]。
    • Stats(GET 页面抠 userBonus/当前火花);端点 syntheses/draw/synthesize/stats/history。
  • Web 面板(MedalSynthesisGame.vue,浅粉/紫/绿对齐原站):顶部栏(卡牌合成 + 当前火花)→ 目标卡(图/名/进度/保底)+ 碎片库存网格(每片 current/required,集齐绿框)→ 单抽/十连/百连/合成/记录 → 上次抽取碎片揭示 → 记录区(碎片/消耗/时间)。
  • 验证:后端 22 包全绿(含 TestParseSyntheses);前端 vue-tsc+build ✅。

移动端富娱乐 UI(Flutter 四玩法)(2026-06-26) ​

对齐 Web,移动端娱乐页从「卡片+玩一次」升级为按玩法分发的富 UI。

  • API(entertainment.dart):加 GameStats/BuyOption/GameRecord/SynthFragment/SynthesisTarget 类型 + stats/buyOptions/buy/history/syntheses/draw/synthesize 方法;PlayResult 加 prizeId。
  • 页重构(entertainment_page.dart):站点选择 + 玩法选择 + 按 gameKey 分发;通用卡兜底。
  • 4 个富组件(features/entertainment/):
    • jgg_game_view.dart:3×3(外圈 8 格顺时针 + 中心按钮),中奖后高亮循环减速 3 圈落定(节奏对齐 Web/Kotlin)。
    • cspt_scratch_view.dart(古风金玉):stats(剩余刮卡/金元宝)+ 刮卡 + 单抽/自动刮卡 + 购卡档位。
    • magic_scratch_view.dart(琥珀):stats(火花/今日次数)+ 刮卡 + 刮一张。
    • card_synth_view.dart(粉/紫):目标卡 + 碎片库存网格 + 单抽/十连/百连/合成/记录。
  • 验证:flutter analyze No issues found! + flutter test 23/23 ✅。
  • 待跟进:① 真机实测各站真实响应(卡牌合成解析、财神登录 token、stats 正则等)校准;② 娱乐页「任务/赠送碎片/万能卡」等次要功能(源码有 get_tasks/gift_fragment/use_universal 等接口)按需补。

Web 跨站搜索页(对齐移动端 + 后端搜索并发化)(2026-06-27) ​

收口「Web 缺跨站搜索页」——后端 POST /search 聚合端点 + 移动端 search_page.dart 早齐备,Web 前端既无 client 也无视图/路由。同时修掉后端一处隐患:SiteChain.SearchSites 注释写「并发」但实现是串行 for、且无超时。

  • 后端并发化(chain/site.go SearchSites):sync.WaitGroup 扇出 + 预分配 results[i] 各 goroutine 写独立下标(无锁)+ chan struct{} 信号量封顶 8 并发 + 每站 context.WithTimeout(15s);保留单站失败写 Error 不影响其它的容错语义。多站搜索从 N×RTT 降到 ~1×RTT。
  • 后端测试(chain/site_search_test.go,复用 fakeAdapter 补零值安全 err/delay + newChainDB 缝):TestSiteChainSearchSitesConcurrent(5×100ms 并发耗时 <400ms 证非串行 + 单站 err 隔离不影响其它)、TestSiteChainSearchSitesTimeout(hang 站被 ctx 取消记 error 不阻塞整体)。go vet/test 全绿;-race 需本地 gcc(当前环境无,但各 goroutine 写独立下标、无共享写,设计上无数据竞争)。
  • 前端 API(新建 api/search.ts):SiteTorrent/SiteSearchResult(snake_case 对齐 site.Torrent/chain.SiteResult,size 为站点已格式化串)+ searchApi.all(keyword)(POST /search 空 body + query keyword)。
  • 前端视图(新建 views/Search.vue):扁平可排序表(站点 chip / 标题+副标题 / 大小 / 做种↑ / 吸血 / Free / 操作,默认按做种降序)+ 失败站点折叠提示(聚合容错)+ 三态(顶层请求失败 ⚠+重试;空态区分「未搜」与「搜了无果」)+ 点「下载」弹下载器选择 → downloaderApi.add 推送(对齐移动端 _download 闭环;download_url 空提示「该站点暂不支持直接下载」)+ 「详情」外链新标签(桌面端增值)。
  • 接线:router/index.ts 加 search 路由;MainLayout.vue navItems 站点后插「搜索」;GlassIcon.vue 加 search 放大镜字形。
  • 验证:后端 go vet ./... + go test ./... 全绿;前端 vue-tsc exit 0 + npm run build ✅(新增 Search-*.js 5.55KB)。

移动端备份页(对齐 Web)(2026-06-27) ​

收口「移动端缺备份」——后端 /backup/{export,import,import-moviepilot} + Web Backup.vue 早齐备,移动端零备份代码。本次完整移植(导出+导入+MoviePilot 导入)。

  • 契约纠偏(实测 backup.go readBody = ShouldBindJSON(&json.RawMessage)):导入是 raw JSON body,不是 multipart——Web 也是「读文件→JSON.parse→POST JSON」。移动端走 JSON body,禁用 postMultipart。
  • ApiClient 补两方法(api_client.dart):① getBytes(path)→Uint8List(导出用,跳过 jsonDecode、401 复刻清 token);② postJson(path, Object body)(导入用,body 类型放宽为 Object 使 List(MoviePilot 数组)也能进 JSON body)。
  • 新建 BackupApi(api/backup.dart):export()/importOwn(Map)→ImportResult{sites,downloaders}/importMoviePilot(List)→int。
  • 新建备份页(features/backup/backup_page.dart):三 GlassCard(导出/导入/MP 导入,文案对齐 Web);导出→Share.shareXFiles([XFile.fromData(bytes,…)]) 系统分享存盘;导入→FilePicker.pickFiles(json, withData:true)→utf8.decode→jsonDecode→按 own(Map)/mp(List) 分发;MissingPluginException 兜底「请用 Web 端」(鸿蒙等无原生实现);SnackBar 反馈、_busy 禁用按钮。
  • 入口:account_page.dart 「登出」上方加「备份」按钮 → Navigator.push(BackupPage)(底部 7 tab 已满,二级 push 同 sites_page→WebViewLogin 惯例)。
  • 依赖:pubspec.yaml 加 share_plus ^10.0.0(系统分享,重导出 XFile;无需单独 cross_file)。
  • 验证:flutter pub get ✅(share_plus 10.1.4)、flutter analyze No issues found!、flutter test 24/24 ✅。

移动端文件管理页(对齐 Web)(2026-06-27) ​

收口「移动端缺文件管理」——后端 /storages/*(list/upload/download/mkdir + storage CRUD)+ Web FileManager.vue 早齐备,移动端零文件管理代码。本次移植:浏览已有存储后端的目录 + 下载 + 上传 + 新建文件夹。无新依赖(复用备份加的 share_plus、file_picker、getBytes/postMultipart)。

  • 后端小修(filemanager.go Download):出错分支由 200+JSON 改 500——原软失败会让客户端把错误 JSON 当成文件字节;改后 getBytes 自然抛 ApiException,Web <a download> 也从「下坏 JSON」变「显错误」。仅 download 一处。
  • 新建 FileStorageApi + 模型(api/file_storage.dart):FileStorage/FileEntry(snake_case 对齐后端)+ list/files/upload/download/mkdir。软失败感知:files 读 error 字段、upload/mkdir 检查 ok 转 Exception(后端软失败返 200+字段,仅 >=400 才触发 ApiException)。
  • 新建文件管理页(features/file_manage/file_manage_page.dart):Scaffold+AppBar(建目录/刷新)+ 存储下拉切换 + Wrap+ActionChip 面包屑(点段截断、上级、根)+ GlassCard 列表行(目录前置、点目录进入、文件显 fmtBytes)+ FAB 上传 + 文件行下载按钮。下载走 getBytes+Share.shareXFiles(抄备份模式,MissingPluginException 兜底);上传走 FilePicker→MultipartFile(抄 downloaders,path 进 multipart fields);建目录走 _prompt 对话框。
  • 入口:account_page.dart 「备份」下加「文件管理」按钮 → 二级 push(不改 home_shell)。
  • 验证:后端 go vet/test 全绿;移动端 flutter analyze No issues found! + flutter test 24/24 ✅。
  • 非目标:不做删除文件(✅ 见下「文件删除补全」);不做存储 CRUD(请在 Web 端配置,无存储时空态提示);上传无进度条。

移动端 NAS 监控(含摄像头实时画面,后端 HLS 改造)(2026-06-27) ​

收口跨端最后一项——后端 NAS 监控(设备/指标/告警/探活)+ Web NasServer.vue 齐备,移动端零监控代码。难点:摄像头实时画面被后端 jessibuca 专有格式(裸 H264 annexB + 4 字节 LE 时间戳 over WS,rtsp_session.go frameMessage)阻塞,标准播放器吃不下。用户选定「含实时画面(需后端改造)」→ 后端加 HLS 转封装出口。

  • 后端 HLS(方案 B,无新外部 Go 包):新建 hls_session.go——hlsSink 用 mediacommon pkg/formats/mpegts 的 mpegts.Writer 把已解出的 H264 AVCC 转 MPEG-TS;IDR 作分段边界,每段新建 Writer + WriteTables() 写段头 PAT/PMT(段可独立解码),DTS 用 h264.DTSExtractor,ring 留最近 5 段 + live m3u8。mediacommon/v2 + go-astits 本是 gortsplib 间接依赖,go mod tidy 升 direct。
  • 后端接入:rtsp_session.go OnPacketRTP 回调 broadcast(...) 重构为 dispatch(ts, nalus)(同时喂 ws jessibuca 帧 + hls sink),缓存原始 SPS/PPS;HLS 观众走租约定时器(每次请求续约 hlsLease,超时释放 sink;与 ws 观众共纳入 viewerKeepWindow 保活与全局上限 8)。rtsp_proxy.go 加 ServePlaylist/ServeSegment + authHLS(复用 ?token=<jwt>,与 stream 同构,media_kit HTTP 不能设 header)。monitor_stream.go + router.go 加 GET /monitor/cameras/:id/hls/playlist.m3u8 + segment/:seq(guarded 外,token 鉴权)。Web 的 WS/jessibuca 链路完全不动。app.go 无需改(复用现有 token)。H.265 仍报错(与 Web 一致)。
  • 后端验证:新建 hls_session_test.go(SPS/PPS/IDR 取自 mediacommon 官方测试向量)——断言合法 H264 → 正确 m3u8(EXTM3U/MEDIA-SEQUENCE/EXTINF)+ TS 段 188 对齐/sync 0x47/首包 PID=0(PAT)+ ring 淘汰。go vet/test ./... 全绿。e2e(真实摄像头 H264 码流)待真实环境——可用 ffmpeg+mediamtx 起本地 RTSP 源 + ffprobe 验证 ts 可解码。
  • 移动端:pubspec.yaml 加 media_kit 族(+ android/ios/windows 原生 lib);main.dart MediaKit.ensureInitialized()(try-catch 鸿蒙跳过);AndroidManifest 补 INTERNET 权限、iOS Info.plist 加 NSLocalNetworkUsageDescription。
  • 移动端 API/页面:新建 api/monitor.dart(MonitorTarget/Metrics/MetricSnapshot/MonitorAlert 模型 + MonitorApi + streamURL(id)→HLS 地址;Metrics 二次 JSON 解析);monitor_page.dart(按 host/nas/camera 分段 + 在线状态点 + 未读告警条 + 增删改入口);monitor_detail_page.dart(探活/编辑/删除 + 指标卡(CPU/内存/磁盘/温度) + fl_chart LineChart 趋势 + 磁盘/温度/网速/负载/运行时长 + 摄像头 media_kit Video 预览(16:9, dispose 释放, 鸿蒙 try-catch 降级 WebView 跳 Web 版) + 设备信息 + 该设备告警);device_form_sheet.dart(type 驱动 protocol/vendor、凭据编辑留空不改)。告警走 HTTP 轮询(对齐 Web 15s;后端无 nas-alert WS)。入口:account_page.dart 加「设备监控」按钮 push。
  • 验证:flutter analyze No issues found! + flutter test 24/24 ✅。
  • 风险/待真实环境:① HLS muxer 单元层验证 TS 结构正确,真实摄像头码流(PTS/IDR 间隔/H265)待实测;② 5-10s 延迟(监控可接受,UI 已标注);③ 鸿蒙无 media_kit 原生 → 降级 WebView;④ H.265 摄像头报错提示切 H.264。

文件删除补全(后端补路由 + Web/mobile 接入)(2026-06-27) ​

补全文件管理缺的删除:后端 chain.Delete 早已就绪,缺的只是 HTTP 路由 + 前端入口。

  • 后端:filemanager.go 加 DeleteFile handler(path 走 query、soft-fail {ok},与 Mkdir 同构);router.go 加 POST /storages/:id/delete。
  • Web:filemanager.ts 加 removeFile(id,path)(soft-fail 检查 ok 转 Error);FileManager.vue 操作列加「删除」按钮(ElMessageBox 二次确认,文件/目录均可删,删除后刷新)。
  • 移动端:file_storage.dart 加 remove(id,path);file_manage_page.dart 行内加删除 IconButton(AlertDialog 二次确认 + 刷新)。
  • 验证:后端 go vet/test 全绿;前端 vue-tsc+build ✅;移动端 flutter analyze No issues + flutter test 24/24 ✅。

安全加固 P3:SSH hostkey TOFU + WS/RTSP 一次性 ticket(2026-06-27) ​

收口安全审查遗留 P3 两项:

A. SSH hostkey TOFU(ssh.go 原全后端唯一 InsecureIgnoreHostKey):监控 SSH 采集无 host key 校验,可被中间人。改为 TOFU——首次连接记录 key、后续校验、不符即拒绝(连接失败 → 设备显离线 + LastError 含 mismatch,复用现有 offline→告警链)。

  • 新建 SSHHostKey 模型(models.go,host+port 复合索引、Key 存 marshaled 公钥不敏感)+ oper/ssh_hostkey.go(Lookup/Record/Delete)+ AutoMigrate。
  • monitor 包 HostKeyStore 接口 + makeHostKeyCallback(注入避免 monitor→oper 反向依赖);NewSSHCollector(keyStore);app.go 注入 oper(结构化满足接口)。
  • AcceptHostKey 端点(合法换 key 一键重置防 footgun):POST /monitors/:id/accept-host-key → 删该 host:port 的 key → 下次连接重新 TOFU。
  • 测试 ssh_hostkey_test.go:ed25519 真公钥 + fake store 断言三态(首次记录/同 key 放行/不同 key 拒绝)+ nil store 降级。

B. WS/RTSP 一次性 ticket(原 ?token=<jwt> 落 URL + 跳过 TokenVersion 校验、登出不踢 WS):JWT 不再落 URL,顺带修登出洞。

  • 新建 auth/ticket.go TicketStore(map[ticket]{uid,exp},crypto/rand 32B、30s TTL、Consume 单次删除、惰性 sweep——复用 ratelimit/token_version 的 TTL map 范式)+ ticket_test.go(单次消费/过期)。
  • 签发端点 POST /auth/ws-ticket(放 guarded 内→走 JWTAuth+TokenVersion,登出后无法再签);WS ws.go + RTSP rtsp_proxy.go HandleWS 改 ?ticket=+Consume。
  • HLS 保持 ?token=<jwt>(media_kit 多段拉取+不能设 header,单次 ticket 不适用;仅移动端内部播放器无 referer 面)。
  • 跨端接 ticket:前端 useWs.ts/RtspPlayer.vue/monitor.ts streamURL + auth.ts wsTicket;移动端 ws_client.dart(HLS streamURL 不动)。
  • 测试 ws_test.go 更新(?ticket=garbage→401)。
  • 验证:后端 go vet/test ./... 全绿;前端 vue-tsc+build ✅;移动端 flutter analyze No issues + flutter test 24/24 ✅。
  • 风险/边界:TOFU 首次盲区(首次连接的中间人仍被记录,TOFU 固有);ticket 单实例(内存 map,与 TokenVersionCache 同约束);登出后旧 WS 长连接仍活到自然断(长连接固有,ticket 只防新连接拿 JWT);HLS 仍 query token(留作后续)。

下载器健壮性加固(qb 401/rid;TR ids 核实排除)(2026-06-27) ​

逐条核实体验测试遗留的「P2 下载器健壮性」三项(非盲改):

  • TR ids → 核实排除(假阳性):tr/client.go 的 rpc 不设请求 tag(同步请求/响应、无错配);Pause/Resume/Delete 把 hashString 放进 ids 是 Transmission 官方支持形式;401=凭证错(Basic Auth 每请求带、无会话过期);无状态→重启即恢复。无真实缺陷,不改(同「限速单位」「NFO」核实排除)。
  • qb 401 自愈(client.go:135):doBody 会话失效重试由 status==403 扩为 403||401(qB 默认 403,反向代理/鉴权变更等可能 401)。httptest 验证首次 401→重登→200 成功。
  • qb rid 重启丢弃(client.go Sync、sync.go:50):qB 重启 rid 回退、若返非 full 增量会被 r.Rid<s.Rid 永久丢弃、卡死旧数据。Sync 加回归检测(r.Rid>0 && r.Rid<reqRid && !FullUpdate)→ 重置状态 + 以 rid=0 全量重拉一次(单次递归、无死循环)。httptest 验证重启后自动全量重拉。
  • 验证:go vet ./... + go test ./internal/downloader/... 全绿(新增 401 自愈 + rid 回退两 httptest 用例,既有不回归)。
  • 真实实例(bonus):连真 qB 重启后看自动全量重拉;401 场景难复现(qB 默认 403),靠 httptest 覆盖。

原项目功能移植 · 第一批:站免 + 公告 + PT监护室(2026-06-27) ​

移植原 Kotlin 项目缺失的 10 个功能的第一批(全 10 个按 4 批排期,逐批确认;本批三端全对齐)。三功能均归入「站点信息聚合」。

  • 站免(Freeleech):检测每站首页全站免费/促销横幅(正在/即将/暂无/失败)。后端走站点 capability 范式(套用 H&R 全链路):FreeleechDef/FreeleechCapable(adapter.go)+ ParseFreeleech(freeleech.go,候选选择器 + 双关键词 AND + isUpcoming + findInlineBackground)+ (*NexusPHP).FetchFreeleech + SiteChain.Freeleech/FreeleechAll(site_info.go,per-site 缓存 30min)+ runFreeleech cron(WS 推送正在站免)。API GET /freeleech + POST /freeleech/sync/:id。
  • 公告(Announcements):抓每站首页公告,结构化标题/日期/正文。ParseAnnouncements 5 级联策略(li.index-news-item 现代 / [id^=ka] 经典 / div#announcement / rowhead 公告行 / 最大文本块兜底)。per-site 缓存 60s。API GET /announcements + POST /announcements/sync/:id。
  • PT监护室(Savept):聚合第三方 savept.icu 站点健康(正常/病危/已关站)+ 病危/恢复/站庆公告 + 抢救时长,与本地站点 4 级匹配(URL→域名→名称精确→名称模糊)。独立 internal/savept/ 包(不依赖站点登录),全局缓存 6h,本地匹配每请求实时算。API GET /savept。cron 每 2h 刷新 + 病危通知。
  • 关键决策:站点为全局共享配置 → per-site 缓存按 siteID 分桶(非 per-user);导出仪表盘图走客户端出图(第四批);savept/savept.icu 抓取 SSRF 守卫已启用。
  • 三端:Web 加 3 视图(Freeleech/Announcements/Savept.vue)+ 3 api 客户端 + 路由 + 导航(GlassIcon.vue 加 freeleech/announcement/savept 字形);Mobile 加 3 feature 页 + 3 api + 账户页「更多功能」入口。
  • 验证:go build/vet/test ./internal/site/... ./internal/savept/... 全绿(新增站免/公告/savept fixture 钉值测试);npm run build 通过;flutter analyze No issues。
  • 文档:新增 09-站点信息聚合.md,更新 00-总览.md。
  • 待跟进:① 真实 NexusPHP 站首页站免/公告横幅实测校准(fixture 现为代表性样例,按选择器校准工作流逐站钉值);② savept.icu 真实页结构校准(断网时用 fixture);③ MTeam 站免/公告(MTeam 走 JSON API,本批 NexusPHP 覆盖,MTeam 暂未实现 capability,graceful 跳过)。

原项目功能移植 · 第二批:消息 + 考核 + 后宫(2026-06-27) ​

第一批(站免/公告/监护室)三端全绿后,移植原项目三个最大的站点抓取功能。复用第一批的 capability 范式,引入两个新架构模式:

  • 后宫(Harem):聚合各站邀请库存(剩余永/临邀请 + 可购买)+ 被邀请人花名册。HaremCapable(adapter.go)+ ParseInviteStatus/ParseInvitees(harem.go)+ cookie userId 提取(cookie.go,c_secure_pass base64→JSON)+ SiteChain.HaremAll(Semaphore(5)+per-site Mutex+30min 缓存)+ 药单导出。MTeam /api/member/profile + /api/invite/getUserInviteHistory。
  • 考核(Assessment):新建 internal/site/assessment/ per-platform 子包 + Registry(NexusPHP HTML 跨行聚合 / MTeam /api/examine/getMyActiveList / TNode 不支持)。DB 持久化 AssessmentTask 表(UNIQUE(site_id,task_name))+ 3 级缓存(内存30min→DB6h→网络)。convertTaskToCard UI 状态阈值按指标计数整数除法。assessment-refresh cron。
  • 消息(Messages):MessageCapable 多方法接口(第一批只读,本批扩展写操作:FetchMessages/Content/Reply/Delete/MarkRead)。CSRF 可选(修正原版默认 reply 必失败的 bug:空 csrfField=不带 token 直接 POST)。多 mailbox(inbox/sent/system/cheater/reports/staff,STAFF 走 staffbox.php)。MTeam 只读(通知 API + 统计伪消息)。
  • 关键决策落地:消息写操作 CSRF 可选化;考核 DB 持久化(进度历史趋势,第四批导出图复用);MTeam 三个功能均走 JSON API 复用 mtRequest(baseURL 已是 api.m-team.cc,无需子域改写);考核状态阈值按指标计数整数除法(非 progress%)。
  • 三端:Web 加 3 视图(Harem 药单表+花名册弹窗/Assessment 进度卡网格/Messages mailbox tab+详情抽屉+回复删除)+ 3 api + 路由 + 导航(GlassIcon 加 harem/assessment/message 字形);Mobile 加 3 feature + 3 api + 账户页入口。
  • 验证:go build/vet/test ./... 全绿(新增后宫/考核/消息 fixture + 考核跨行聚合 + MTeam 拼写错误字段测试);npm run build 通过;flutter analyze No issues。
  • 文档:新增 10-站点用户管理.md,更新 00-总览.md。
  • 待校准(真实环境):① 后宫 cookie userId 提取 + invitee 禁用属性检测;② 考核 NexusPHP userdetails 跨行聚合 + MTeam examine 字段;③ 消息列表/正文选择器 + 回复 CSRF 字段名(各站不同,需站点 YAML messages.csrf_field 配置)。

原项目功能移植 · 第三批:种子浏览(详情页) + 推荐 + 探索(2026-06-27) ​

第二批(消息/考核/后宫)三端全绿后,移植最后三个最大的站点/媒体功能。两个架构决策(与用户确认锁定):① 按需联动——推荐/探索展示媒体元数据,「在站点查找」复用既有 POST /search(标题当 keyword),不预过滤、无 N×M、零后端改动;② 信息流 vs 搜索——推荐=多源热门信息流,探索=关键词跨源搜索,共享联动基础设施。

  • 种子浏览(详情页):跨站搜索 → 点击单种看详情页。TorrentDetailCapable(adapter.go,type-assert)+ DetailDef/TorrentDetail(types.go)+ DefaultNexusPHPDetail(defaults.go,details.php 选择器)+ ParseTorrentDetail/extractMulti(seedetail.go,复用 Extract/ApplyFilters/ResolveURL)。NexusPHP(直接 GET 绝对 URL + loggedOut)+ MTeam(/api/torrent/detail {sn},schemas/seedetail.go)。顺手修 MTeam bug:parseMTTorrent 补 DetailURL(原遗漏)。SiteChain.TorrentDetail(sameHost SSRF 守卫比对最后两段注册域 + 10min 缓存)。
  • 推荐:新增 TrendingCapable 可选接口(media.go,不破坏 base MediaSource,MusicBrainz 跳过)+ 各源 Trending(豆瓣 /movie/hot·/coming·/tv/hot / TMDB /trending/{movie,tv}/week·/movie/popular / Bangumi sort=heat)+ RecommendChain(recommend.go,仅内存缓存 30min 无 DB,并发扇出 semaphore 4 + 容错合并 + force 绕缓存)。API GET /media/recommend。
  • 探索:MediaHandler.Discover 直接复用既有 MediaChain.Search(跨源合并 + PosterCache),source 返回后过滤——零新 chain。API GET /media/discover。
  • Bangumi 客户端(bangumi.go):v0 API(可选 Bearer token),Search(filter.type [2,6] 动漫+三次元)/ Detail / Trending。无需扩 MediaInfo(既有 Source/Type 够用)。配置 BANGUMI_TOKEN;图片反代 mediaImageHosts 加 lain.bgm.tv/bgm.tv。
  • 三端:Web 加 3 视图(SeedBrowse 跨站搜索表+详情抽屉 / Recommend 卡片网格+源/分类 chip+在站点查找 / Discover 搜索框+媒体卡+在站点查找)+ 3 api + 路由 + 导航(GlassIcon 加 seedbrowse/recommend/discover 字形);Mobile 加 3 feature 页 + 3 api + 账户页入口(7 底部 tab 满,二级 push)+ 共享 widget media_poster.dart/find_on_sites_sheet.dart。
  • 验证:go build/vet/test ./... 全绿(新增详情 fixture + MTeam sn 抽取 + parseMTTorrent DetailURL 回归 + Bangumi search/detail/年容差 + 三源 Trending + RecommendChain 合并/缓存/force/过滤);npm run build ✅;flutter analyze No issues。
  • 文档:新增 11-种子浏览与媒体发现.md,更新 00-总览.md。
  • 待校准(真实环境):① 种子详情 NexusPHP Detail.Fields 各站选择器(/sites/inspect 校准);② MTeam /api/torrent/detail 端点/sn/字段名;③ Bangumi rating 形态;④ 豆瓣 frodo 热门路径与响应结构;⑤ 各源 Trending 字段。

原项目功能移植 · 第四批:导出总仪表盘图片(2026-06-27) ​

收口最后一批——导出仪表盘为可分享图片。架构决策(早前锁定):走客户端出图,无后端改动(仪表盘数据既有 /dashboard,导出纯前端)。捕获范围 = 总览摘要(指标 + 近 7 天趋势),非滚动站点明细表(表格不适合出图)。零新增依赖。

  • Web(utils/dashboardImage.ts):零依赖 Canvas 2D 绘制深色品牌分享卡(紫蓝渐变背景 + 标题时间戳 + 5 指标块[色条+自适应字号] + 近 7 天柱状图[渐变+值标签+日期] + 页脚),@2x 渲染清晰;exportDashboardImage() → canvas.toBlob 触发 PNG 下载。不引入 html2canvas(项目刻意保持依赖精简,echarts 已移除)。Dashboard.vue page-head 加「导出图片」按钮(download 字形)。
  • Mobile(dashboard_page.dart):RepaintBoundary.toImage 截分享卡 widget → PNG → 复用既有 share_plus(XFile.fromData,同备份导出范式)分享/保存。点「导出图片」弹预览底栏(_shareCard 在 RepaintBoundary 内,已 layout+paint)+「分享」按钮 → toImage(pixelRatio:3)。分享卡用手动柱状图(不依赖 fl_chart,截图可靠)。MissingPluginException 兜底「请用 Web 端」(鸿蒙等)。
  • 验证:前端 vue-tsc+npm run build ✅;移动端 flutter analyze No issues。
  • 非目标:不导出滚动站点明细表(适合 CSV,不适合图);Web 端为下载、移动端为系统分享(各自平台惯用法)。
包测试
httpclientSSRF IP + retry + CF + cookie + ssrf dial 守卫(激活拦截私网/未激活降级)
core/event广播扇出 + 链式顺序 + panic 隔离
core/moduleSkip/Continue/Stop + 取消
core/securityAES 往返 + bcrypt + JWT 往返/篡改/过期
core/configrelease 强校验(缺/默认值拒绝 + 强密钥通过 + dev 回退)
auth激活码生成/校验/激活/过期/吊销/复用
chainRunModule 三语义 + SiteChain 调度 + ErrNoAdapter + SearchSites 并发(耗时证非串行+err 隔离)+每站超时(ctx 取消 hang 站) + RecommendChain(多源 Trending 合并 + 二次命中缓存源不重复调 + force 绕缓存 + source 过滤 + 无 Trending 能力源跳过)
siteselector 提取/过滤器/ParseBytes(含 TiB/GiB) + Selectors 回退/Parent/:::N 取组 + search fixture + nologin + IsVIP + hxpt 校准(2) + HR 解析(ParseHR/时长/危险判定) + 三站真实页面 fixture 钉死(hxpt/cspt/crabpt usercp+userdetails:color_* 锚点 + rowhead level/join + uid 发现) + 站免(ParseFreeleech 双关键词 AND) + 公告(ParseAnnouncements 5 级联) + 后宫(ParseInviteStatus 4 策略 + ParseInvitees 流量/分享率/禁用属性 + IsDead/IsLowRatio 计算字段 + cookie userId base64 提取) + 消息(ParseMessages 多 mailbox + 日期探测 + unread + SynthesizeMsgID + ParseMessageContent 4 策略) + 种子详情(ParseTorrentDetail fixture:简介/截图[]/IMDBID/DoubanID/DownloadURL 绝对化 + DefaultNexusPHPDetail + EnsureDefaults 填充 + 多值 extractMulti)
site/schemasnexusphp httptest(搜索+登录检测) + keepalive(Set-Cookie 轮换+失效) + hr(抓取+登出) + mteam(canned JSON 搜索/profile + 401/业务错误) + nexusphp 多页 FetchUserInfo(usercp→uid→userdetails/mybonus/getusertorrentlistajax 聚合 + 子页 404 容错 + seeding_size 逐单元格累加) + 后宫(invite.php 翻页≤20 + MTeam profile/invitee JSON) + 消息(多 mailbox + 正文 + 写操作 CSRF 可选 + MTeam 通知/统计伪消息) + 种子详情(NexusPHP 直接 GET 绝对 URL + MTeam /api/torrent/detail JSON 映射 + mtSNFromDetailURL 末段/query 抽 id + parseMTTorrent 补 DetailURL 回归)
site/assessmentper-platform Registry(NoOp 兜底) + NexusPHP HTML 跨行聚合(normalizeHTML <td>→换行 + mergeAssessmentLines + header A/B + 指标 Phase1 单行/Phase2 逐行 + 结果 isResultCompleted 排除「未达标」+ 单位归一 size→GB/time→小时 + endTime 不可解析→UNKNOWN) + MTeam examine(拼写错误字段 uploaed/downloaed/bonuses) + TNode 不支持
db/opersite AES-GCM + registration CRUD/状态流转 + URL scheme 白名单 + assessment BatchUpsert(事务删后插)/CleanupExpired/LatestFetchedAt
chain(registration)signup 开放/关闭判定 + 转换去重
downloader/qbrid merge(全量/增量/过期) + httptest(登录/版本/同步/添加/分支)
downloader/tr409 session 握手 + torrent-get
cookiecloudAES 往返 + host 匹配
mediaTMDB httptest + NFO + ErrNotConfigured + 豆瓣(canned frodo 搜索/详情 + 微信 UA/apiKey 注入 + 年份过滤 + subjects.items 改版适配) + posterURL(m_ratio_poster 规整) + Bangumi(search/detail/年容差 + Trending heat) + 三源 Trending(豆瓣 hot/coming/tv + TMDB trending/popular + 未配置 ErrNotConfigured)
api/v1media 图片反代白名单(mediaImageReferer:doubanio/tmdb/bgm 放行 + 后缀伪装/SSRF 拒绝)
filemanagerWebDAV httptest(列目录/上传/下载) + BasePath resolvePath(越界约束)
keepseed评分(相同/悬殊/无关/空)
notification扇出隔离 + webhook httptest
mobile搜索 + 仪表盘 + WS 推送 + 媒体 + 娱乐模型 fromJson(/search、/dashboard、/dashboard/trend、ws 种子快照+H&R+cookie、/media/search、/entertainment 含缺字段容错) + MediaInfo.detailUrl(豆瓣/TMDB/MB 映射+缺 id 容错) + 站免/公告/savept 模型 fromJson(状态/匹配枚举容错) + 种子详情/推荐/探索模型 fromJson(TorrentDetail + MediaInfo + 缺字段容错) + 辅种 reseed(ReseedTask 模型 + reseedApi) + 冒烟测试
saveptparseSavept(卡片 data-* 抽取 + 状态归一 + 抢救时长回填 + 公告切片) + matchCards(4 级匹配优先级 + 已绑定回填) + summarize + normalizeStatus 关键词
wsHRAlert/Cookie/Freeleech 编码(hr-alert/cookie-alert/freeleech-alert) + torrents 消息(any Data 兼容)
entertainment4 游戏 × 成功/失败 + Registry
api/middlewareratelimit burst + per-key + 桶空闲回收 + 安全响应头
api端到端用户旅程(httptest: health→setup→login→建站→下载器→通知→备份→VIP→SPA) + WS 鉴权(无/坏 token→401)

IYUU 精确辅种(infohash 跨站匹配 + 一键添加)(2026-06-28) ​

既有 keepseed 仅名称模糊匹配(keepseed.Score Jaccard+大小),包注释明写「对接 IYUU 精确 infoHash 匹配作为后续增强」。本轮实现精确辅种闭环。协议参考 Go 实现 goreseed + api.iyuu.cn(已核实):sign 字段即 IYUU token(无额外签名);客户端按站点目录 + torrent_id + 本地 passkey 拼 download.php?id=&passkey= 直链。

  • IYUU 客户端(新建 iyuu/iyuu.go):HashIndex(token, hashes) POST App.Api.Hash(multipart,sign=token)→ 每 hash 的 [{sid, torrent_id, info_hash}];Sites(token) POST App.Api.Sites → 站点目录;BuildDownloadURL 移植 goreseed 平台分支(默认 NexusPHP / m-team &https=1 / ttg / ccfbits)。复用共享 httpclient(SSRF 守卫对公网 api.iyuu.cn 放行)。httptest 测试钉死响应 + URL 拼装各分支。
  • Chain(keepseed.go):Reseed(dlID) 取下载器做种 hash(需 save_path 复用路径)→ IYUU 匹配 → 按 base_url host 匹配本地 Site → 解密 PasskeyEnc(新增 siteOp.DecryptedPasskey)→ 拼 URL → 落 CrossSeedTask{status=pending}(同 hash+候选站去重);Resolve(taskID, add|skip):add 走 AddTorrent(URL, SavePath, Paused)。token 存 SystemConfig(新建 SystemConfigOper,对齐「runtime settings in DB」)。
  • 可测性:chain 下载器依赖抽成窄接口 dlProvider/reseedClient(仅 ListTorrents+AddTorrent)+ ManagerProvider 适配真实 manager——测试注入 2 方法 fake,不必实现完整 Downloader 接口。
  • API(keepseed.go):GET/PUT /keepseed/config(token 掩码读/明文写)、POST /keepseed/reseed、GET /keepseed/tasks?status=、POST /keepseed/tasks/:id/resolve;保留 POST /keepseed/find。
  • Web:新建 Reseed.vue + api/reseed.ts + 路由/侧栏「辅种」项——配置 token、选下载器开始辅种、候选任务表(状态筛选 + 添加辅种/跳过)。
  • 模型:CrossSeedTask 加 downloader_id/save_path。
  • 移动端收尾:新建 api/reseed.dart(ReseedTask 模型 + ReseedApi)+ reseed_page.dart(token 配置 + 下载器下拉 + 开始辅种 + 任务卡列表[状态筛选 + 添加辅种/跳过]),account_page 加「辅种」入口。复用后端 /keepseed/* API,对齐 Web Reseed.vue。flutter analyze 全绿。
  • 定时自动辅种 cron(2026-06-28 补):CrossSeedSetting 加 AutoReseed/AutoReseedAdd(per-downloader);chain/keepseed.go 加 Setting/SaveSetting/ListAutoReseedSettings;新建 automation/keepseed.go 的 runKeepSeed——遍历 auto_reseed=true 的下载器重跑 Reseed 刷新 pending,AutoReseedAdd 开则对 created 逐个 Resolve("add")(默认非破坏:无启用即零动作、auto-add 默认关)。automation.Service 依赖收窄为 keepSeedRunner 接口(测试注入 fake)。配置 KEEPSEED_CRON(默认 0 3 * * *)。API GET/PUT /keepseed/setting;Web/Mobile「开始辅种」卡各加「定时自动辅种/自动添加」两开关。GORM 坑:PauseAfterAdd 的 default:true 使 bool false=零值落库被改写成 true → SaveSetting 新建走 map-based Updates 强制写零值。chain/automation 测试覆盖 scan-only/auto-add/零启用 三路径。
  • MTeam 走 API genDlToken 下载(2026-06-28 补):MTeam 禁 passkey 直链,原辅种对 MTeam fails。mteam.go 加 GenDlToken(multipart/form-data id + x-api-key POST /api/torrent/genDlToken,envelope data = 签名 URL 字符串;契约源 pt_mate + MoviePilot)。URL 有时效 → 添加时(Resolve)现取:CrossSeedTask 加 SiteID;Reseed 对 local.Platform=="mteam" 存哨兵 mteam:<id>(复用现有 siteOp.APIKey() 解密判凭据),Resolve 对哨兵按 SiteID 取站 → genDlToken → AddTorrent;其它站 passkey 流程不变。chain 依赖收窄 mtTokenGen 接口(测试注入 fake)。mteam_test + keepseed_test 全绿。
  • 仍不在本期:无——IYUU 辅种 deferred 项全部完成,剩真实环境联调(用户 MTeam api_key + iyuu.cn 绑站 + 有做种下载器)。
  • 验证:go test ./internal/... 全绿(iyuu httptest + chain Reseed/Resolve 链路 fake downloader);Web vue-tsc + npm run build ✅。离线仅验证协议拼装与链路;真实 IYUU 匹配需用户 token + 已绑定站点 + 有做种的下载器实测。

娱乐:NovaHD 三娱乐适配(魔力农场 / 刮刮乐 / 幸运抽奖)(2026-06-29) ​

NovaHD(pt.novahd.top,NexusPHP)此前无娱乐源。用户提供站点源码备份(F:\新建文件夹 (3)\好学备份\pt.novahd.top\nexusphp)+ 页面 HTML(D:\html),据此精确适配三个娱乐功能。三个均纯 Cookie 鉴权,货币 = users.seedbonus(魔力值),注册 host=novahd.top(后缀匹配 pt.novahd.top)。

  • 魔力农场(gameKey=novahd_farm,magic_fram.php):站点为 GET + 整页 HTML(无 JSON API),故新增 Farmable 能力接口(Farm/Plant/Harvest/HarvestAll/Sell,section=crop→plant / animal→breed)。配置源码写死(Go 端常量化):小麦4h/500、玉米8h/1000、花生12h/1500、土豆24h/2000(crop expire 2 天);鸡24h/1000、猪48h/2000、羊72h/5000、牛96h/10000(animal expire 5 天)。状态由页面链接判定(含 harvest 链接=ready / plant·breed=idle / 否则 growing);余额抠 .points-display、提示抠 .message、仓库抠 warehouse-table、菜市场抠两张 market-table(第一张 crop 其后 animal)。
  • 魔力值刮刮乐(novahd_scratch,magic_scratch.php):buy(POST→HTML 抠 currentScratchId)→scratch(POST→JSON {success,prize,prize_level,scratch_id})→settle(GET)。6 档常量化:未中奖40%/五等500/四等1000/三等3000/二等5000/一等100000,cost=5000 daily_limit=5。复用 LotteryGame+Statable+Prizeable+Historable(与 hxpt MagicScratchGame 同构)。
  • 幸运抽奖(novahd_lucky,fortune-wheel-spin.php):fortune-wheel-plugin 是 Laravel 商业授权插件(license.dat + 域名/机器码校验;PTPatronus 代理请求站点自身端点不受影响)。POST count=1|10|20|50 → JSON {success, results:[{status:awarded|nothing, type:bonus|attendance_card|upload|nothing, value, unit}]};顶层 PlayResult.Amount 仅累加 type=bonus(补签卡/上传 value 是其他单位)。
  • 三端:后端 6 处同步(entertainment.go 接口+DTO → novahd.go → chain 分发+spendGuard → handler → router → app.go Register)+ novahd_test.go(httptest mock);Web 3 富组件(NovaHDFarmGame.vue/NovaHDScratchGame.vue/NovaHDLuckyGame.vue)+ Entertainment.vue 分发;Flutter 3 View + entertainment_page.dart 分发。
  • 验证:后端 go test ./internal/entertainment/... + go build ./... 全绿;Web npm run build(vue-tsc)✅;Flutter flutter analyze No issues found!。
  • 待真实页面校准:① 农场余额/仓库/菜市场 HTML 抠取正则(对照 D:\html 真实页面);② 刮刮乐 Historable(记录区)、Stats(余额/今日剩余);③ 抽奖 Stats(消耗/今日,渲染在 fortune-wheel-plugin/resources/views/wheel.php)、Prizes(grades)、Historable——目前宽松抠取或返回空,待联调校准。

娱乐:好学农场接入(魔力农场通用化)(2026-06-29) ​

好学(hxpt)也有"魔力农场",源码 F:\www.hxpt.org\public\magic_fram.php(1009 行)与 NovaHD(872 行)对比:同一套插件的不同版本——name/grow/price 配置、操作 action(plant/breed/harvest/harvest_all/sell)、渲染骨架(farm-item/harvest-btn/plant-btn/breed-btn)完全一致;差异仅货币名(好学"火花"/NovaHD"魔力值",余额抠 .points-display 数字不依赖文本)、农作物有效期(好学5天/NovaHD2天)、好学无 img 字段(用 pic/farm/{name}.png)、市场规则文本(好学±50%每日6刷/NovaHD±20%每日1刷)。

  • 通用化:NovaHDFarmGame → MagicFarmGame(gameKey novahd_farm→magic_farm),注册 hxpt.org + novahd.top 两 host 共用一个 Game + 一个富组件。
  • expire 通用化:农作物/动物有效期两站不同,改为从页面 section 标题抠(magicFarmExpireDays:「农作物有效期N天」/「动物有效期N天」),不再写死常量;magicFarmItemDef 去掉 ExpireDays 字段。
  • 三端改名:Web NovaHDFarmGame.vue→MagicFarmGame.vue、Flutter novahd_farm_view→magic_farm_view(类 MagicFarmView)、Entertainment.vue / entertainment_page.dart 分发 key 改 magic_farm;测试加 expire 抠取断言。
  • 验证:go test ./internal/entertainment/... + go build / Web npm run build / Flutter flutter analyze 三端全绿。

娱乐:好学九宫格 连抽(10/20/50/100)+ 抽奖历史(2026-06-30) ​

九宫格(jgg,hxpt)此前仅单抽。站点源码 F:\www.hxpt.org\public\jgg\magic_grid_bulk.php + jgg.php(get_history/get_stats)证实站点原生支持连抽与历史,对照 Kotlin 旧版 HxptJggGame.kt 适配。

  • 连抽(JggGame 实现 MultiPlayable):POST /jgg/magic_grid_bulk.php form bulk_count=N,站点服务端白名单仅 [10,20,50,100](Go 端同步拦截非法值)。响应 {success, results:[{prize,message}], prize_stats:{name:count}, total_cost, new_balance};每次 prize 聚合进 PlayResult.Prizes(前端按 prize_name 分组统计,等价站点 prize_stats)。奖品 value 按类型换算(jggPrizeValue:upload/download 字节→GB、vip/noad 秒→天、invite→个),对齐站点 formatPrizeValue。
  • 历史(JggGame 实现 Historable):POST /jgg.php action=get_history → {history:[{prize_name,prize_type,prize_value,cost,created_at}]}。prize_value 是 PHP serialize 标量(如 i:10737418240;=10GB),新增 phpSerializedNum(正则 ^[id]:(-?[0-9.]+);)还原后再走 jggPrizeValue 换算。
  • 零接线:chain/API/router 的 play-multi+history 早就是按能力接口分发的通用路由,本次只给 JggGame 加两个方法即自动暴露(resolve()+类型断言,spendGuard 防双击)。
  • 三端 UI:Web JggGameUi.vue + Flutter jgg_game_view.dart 加「🎀10/💖20/✨50/🌸100 连抽」按钮(2×2)+ 连抽结果面板(分组统计 × 消耗/余额)+ 📜抽奖历史区(prize_name/-cost/时间,最多 20 条)。连抽无单一中奖格,仅做装饰性外圈流转反馈(区别于单抽落到中奖格)。
  • 验证:后端 go test ./internal/entertainment/ ./internal/chain/(含 TestJggGamePlayMulti/TestJggGameHistory/非法次数拦截)+ go build ./... 全绿;Web vue-tsc --noEmit ✅;Flutter flutter analyze No issues found!。

MoviePilot 第三方插件生态移植(2026-07-12 起,7 批进行中) ​

盘点 59 个 MoviePilot 第三方插件仓库,对照已实现功能识别出 17 个「值得移植」的缺口功能,按 P0–P3 全做、三端对齐、验证=编译+单测+mock(真实联调留用户)。完整计划见会话 plan。每批 1 commit 不 push。

  • 批次1(commit 61ac5f9/abaadd1/5c5437a,PT 核心):刷流引擎(BrushFlowTask/Seed + chain 选种/扫删/cron,tag brushflow:<id> 识别规避跨系统 infohash 匹配)+ 未辅种检查(KeepSeedChain.FindMissing 复用 iyuu 查漏,与辅种互补)+ 保种契约(ContractRule per-site 下限 + evalBreaches 纯函数 + UserInfo 现场比对 + 当天去重通知 + 每日 9 点 cron)。三端 + glyph brushflow/contract。
  • 批次2(媒体服务器基础,2026-07-12):
    • 新包 internal/mediaserver/:统一 Client 接口 + embyJellyfinClient(共源 REST,X-Emby-Token 头鉴权,/Library/MediaFolders 列库、POST /Items/{id}/Refresh?Recursive=true 扫库、POST /Notifications/Admin 通知、/Items?ParentId= 取近期条目 + /Items/{id}/Images/Primary 海报)+ plexClient(/library/sections + X-Plex-Token、GET /library/sections/{id}/refresh 扫描、无通知端点返明确错误、/library/metadata/{rk}/poster 海报)+ 海报墙封面 GenerateCover:area-average 降采样拼网格 JPEG,纯标准库 image 无第三方依赖(缺图跳过留深色背景)。
    • MediaServer model(多实例 CRUD,同 Downloader 范式;token 加密往返;事件开关 per-server RefreshOnDownload/NotifyOnPlay)+ oper(类型白名单 emby|jellyfin|plex,包 ErrInvalidInput 供 handler 判 400)+ AutoMigrate。
    • MediaServerChain:CRUD 透传 + 按需构建 client(解密凭据,内网 AllowPrivate HTTP 客户端)+ 库扫描/刷新/通知/封面 + OnDownloadCompleted(下载完成→扫所有 RefreshOnDownload=true 服务器,best-effort 单失败不阻断)+ ReceiveWebhook(Emby/JF/Plex 播放回调解析标题→通知中心)。
    • API /mediaserver/*(CRUD + /test + /libraries + /scan + /refresh + /notify + /cover)+ 公开 /mediaserver/:id/webhook(媒体服务器无法带 JWT)。
    • 下载完成→扫库接线:ws.Broadcaster 加 SetDownloadDoneHook(同 SetNotice 注入范式,detectDone 内持锁调用),app.go 接线 mediaServerCh.OnDownloadCompleted(非 bus 事件,避免改 broadcaster 构造签名)。
    • 三端:Web MediaServer.vue(卡片 + CRUD + 连接测试 + 库管理 sheet + 封面 blob 预览)+ Flutter media_server_page(卡片 + 表单 + 库 sheet + Image.memory 封面)+ glyph mediaserver 两端 1:1 同步。
    • 测试全绿:mediaserver(Emby httptest 全流程 + Plex section id + Plex 通知不支持 + GenerateCover 端到端尺寸/JPEG/坏图跳过 + resizeImage 纯色保真);chain(parseMediaWebhook 4 形态 + oper CRUD token 往返 + OnDownloadCompleted 触发 mock 扫库)。
  • 批次3(云盘 + STRM,2026-07-12):
    • 新包 internal/cloud/:统一 Client 接口 + 三家原生 HTTP 客户端 —— client115(cookie 鉴权,按 cid 列目录 webapi.115.com/files,无 sha1=目录)+ clientQuark(cookie 鉴权,按 pdir_fid 列目录 drive-pc.quark.cn,file_type=0=目录)+ client123(123云盘官方 API,Authorization: Bearer token 鉴权,按 parentFileId 列目录,code=0 成功)。三家网盘均为公网站点 → SSRF 守卫客户端(AllowPrivate=false,Do(ssrf=true));按「路径」语义列目录(内部 resolvePath 从根 id 逐级按名解析 + 翻页)。字段名/端点基于社区逆向契约,单测用 httptest mock 覆盖,真实联调留用户(代码标 TODO 联调)。
    • STRM 生成器 cloud.Generate:遍历云盘目录树(递归可选)→ 按扩展名识别媒体文件(mkv/mp4/ts/iso 等)→ 按模板生成本地 .strm(默认 {mount}{cloudpath},支持 {mount}/{cloudpath}/{relpath}/{filename})→ 本地幂等(已存在且内容相同跳过)+ 软上限防失控 + 单文件失败不阻断。
    • CloudDrive/StrmRule/StrmRecord 三 model(多实例 CRUD,同 MediaServer 范式;cookie/token 加密往返;StrmRecord drive_id+cloud_path 唯一索引去重)+ oper(类型白名单 115|quark|123,级联删规则+记录)+ AutoMigrate + CloudStrmCron(默认每 6 小时)。
    • CloudChain:CRUD 透传 + 按需构建 client(解密凭据)+ 浏览/配额/探活 + GenerateStrm/GenerateAll(规则驱动,落库 StrmRecord)+ 订阅追更 SyncSubscriptions(复用 SubscribeChain 列订阅,按文件名归一化匹配订阅标题→链接记录 + 通知中心;归一化剥离年份/分辨率/编码噪声)。
    • filemanager 云盘后端:cloudBackend 适配器把云盘 client 暴露为 StorageBackend(只读 List,写操作返 ErrCloudReadOnly),buildBackend 加 115/quark/123 case,验证层放行(网盘为固定公网端点,无自定义 URL)。文件管理器可浏览云盘资源。
    • API /cloud/*(子分组 /drives /rules /records 避免 :id 与静态段冲突):CRUD + /test + /quota + /list(浏览)+ 规则 /generate + /generate-all + /sync-subscriptions。
    • cron:cloud-strm(每 6 小时 → GenerateAll + SyncSubscriptions,单规则失败不阻断)。
    • 三端:Web Cloud.vue(三 Tab:网盘卡片+CRUD+测试+容量+浏览 / STRM 规则表+CRUD+生成 / STRM 记录表,浏览弹窗选目录)+ Flutter cloud_page(TabController 三 Tab + 卡片/列表 + showAdaptiveSheet 表单+浏览 + 追更/生成全部)+ glyph cloud(云朵)两端 1:1 同步。导航「基础设施」组,蓝青渐变 #4cc9f0→#4361ee。
    • 测试全绿:cloud(cleanCloudPath/splitSegments/IsMediaFile/RenderTemplate 纯函数 + 115/夸克/123 httptest 列目录+路径解析+Bearer 头 + STRM 生成器 fakeClient 端到端写入/幂等/非递归/去扩展名);chain(CloudDrive CRUD+token 往返+白名单 + StrmRule/Record CRUD+Upsert + normalizeName/matchAnySubscription + 删盘级联)。go build ./... + go test ./... / vue-tsc + npm build / dart analyze 全绿。
  • 批次4(字幕 + AI字幕,2026-07-12):
    • 新包 internal/subtitle/:统一 Provider 接口 + 四源 —— assrtProvider(ASSRT 官方 API,token 鉴权,/v1/sub/search+/v1/sub/detail 取 zip)+ opensubtitlesProvider(官方 REST,Api-Key 头,/api/v1/subtitles 搜 + /api/v1/download 换直链)+ subhdProvider/zimukuProvider(goquery 抓取,社区逆向选择器,标 TODO 联调)。四源均公网站点走 SSRF 守卫客户端。
    • 下载管线 subtitle.Extract(zip 魔数检测 + archive/zip 取字幕文件,RAR/7z 返明确错误)+ DecodeUTF8(GBK/GB18030→UTF-8,国内站常见)+ Convert(OpenCC 繁简,github.com/longbridgeapp/opencc 纯 Go 内嵌字典,t2s/s2t 缓存实例)。仅对文本字幕(srt/ass/ssa/vtt)+ 源语言与目标不同时转换。
    • SubtitleSourceProvider(SystemConfig JSON blob subtitle.* 键 + env 默认 ASSRT_TOKEN/OPENSUBTITLES_KEY,ApplySettings 即时 rebuild,同 MediaSourceProvider 范式)+ SubtitleChain(并发跨源搜索 singleflight-like 聚合 + 单源失败不阻断 + 按 Score 降序 + 语言过滤;Download 解 zip→UTF-8→繁简)。
    • API /subtitle/search({results:[...]})+ /subtitle/download(POST 字幕标识 → 附件返回字节)+ /subtitle/config GET/PUT(secret 脱敏 + _configured + 空字段保留)。
    • 三端:Web Subtitle.vue(搜索栏+语言筛选+结果列表+下载触发浏览器另存 + 源设置弹窗)+ Flutter subtitle_page(搜索+结果列表+postBytes 下载经 path_provider+share_plus 分享 + 源设置 sheet)+ glyph subtitle(对话气泡)两端 1:1。ApiClient 加 postBytes(getBytes 的 POST 对偶,文件流下载)。导航「基础设施」组,蓝紫渐变 #4361ee→#7209b7。
    • AI字幕市场插件 market-plugins/ptp-ai-subtitle/:external-http Python 插件,faster-whisper 转录生成 .srt + 可选 OpenAI 兼容接口翻译(双语/单语);actions transcribe/scan/status + Schedule;pluginctl package 签名(Ed25519,复用 examples/plugin-market/keys)+ 入市场索引(4 官方插件);TestRealMarketFetchAndInstall 验全 4 真安装链。
    • 测试全绿:subtitle(normalizeLang 双语优先/zh-tw→cht + IsSubtitleExt + Extract zip/直传 + DecodeUTF8 GBK 往返 + Convert t2s/s2t/noop);providers(ASSRT/OpenSubtitles httptest + Api-Key 头 + ErrNotConfigured + SubHD/Zimuku mock HTML 去重);chain(并发聚合+失败源跳过+Score 排序+语言过滤 + Download 繁转简端到端 + shouldConvert 表)。go build + test / vue-tsc + npm build / dart analyze + flutter build windows 全绿。
  • 批次5(Emby 进阶 + 多评分,commit 99e722e,2026-07-12):
    • Emby 进阶:mediaserver.Client 扩展 Users/PlayedItems/LibraryItems(Emby/JF IsPlayed 过滤 + ProviderIds + PlayCount/LastPlayedDate;Plex 历史需 Pass 标 TODO)→ 观影报告 PlaybackReport(枚举用户→各取最近 N→聚合用户/热门/最近/总播放,days 窗口)+ 重复检测 DetectDuplicates(TMDB>IMDb ID 优先,回退 normalizeTitle+年份,Count>=2 组)+ 302 直链反代 GET /mediaserver/:id/direct/*path(公开静态改写 DirectLinkBase 不泄露 Token,对标 MP Emby 直链省本机带宽)。
    • 多评分:MediaInfo 加 VoteCount(TMDB/豆瓣/Bangumi 三源)+ 新增 OMDb IMDb 源(?t 最佳匹配 + parseVotes/parseFloat 解析 "500,000"/N/A",OMDB_API_KEY 空=跳过)→ MediaChain.AggregatedRating(并发跨源按标题+年份取每源投票最高 match,投票数加权综合,6h 内存缓存避订阅击穿)+ 三道闸门 evaluateGates(min_score/min_votes/min_release_days)+ 订阅低分保护(Subscription EnableRatingGuard,全局 MediaQualityConfig 与单订阅开关取并集,查询失败 fail-open)。API /media/quality GET/PUT + /media/rating 预览 + /mediaserver/:id/playback-report|duplicates。三端 glyph mediaquality(星形)红橙渐变。
    • 测试全绿(EmbyAdvanced httptest + DetectDuplicates 归并 + OMDb 三源 + evaluateGates 三关 + AggregatedRating 加权 + subscribe ratingGate 四态)。
  • 批次6(通知渠道补全 + 论坛签到扩展,commit 9e2c2c6,2026-07-12):
    • 通知渠道补全:internal/notification/ 新增 11 通道(PushPlus/PushDeer/ntfy/gotify/Apprise/WorkWechat/WxPusher/Lark/Discord/MQTT/Server酱);MQTT 纯标准库自实现 3.1.1 报文(CONNECT/PUBLISH QoS0/DISCONNECT,不引 paho);NotificationSettings 扩 11 子结构 + provider rebuild(秘密字段空串=保留)+ API GetConfig/SetConfig(maskSecret + *_configured);三端设置页 11 el-input 卡片。
    • 论坛签到扩展:新子包 internal/plugins/forumsignin/(6 站 handler:NodeSeek JSON + 蜂巢/飞牛/柠檬/国语视界/SSDForum 共享 nexusSignin 基类 + classifySigninBody 分类)+ Engine(逐站签到+composeReport 通知+失败重试 KV+time.AfterFunc)+ forum_signin.go 插件入口(ConfigSchema + Schedule 每日 8 点)+ manager 注册 ptp-forum-signin。凭据由插件配置承载(key|base|cookie[|path] 每行)。
    • 测试全绿(channels_extra httptest 10 通道 + MQTT 本地 broker done channel 避竞态 + forumsignin NodeSeek/nexusSignin/Engine 端到端)。
  • 批次7(Plex 套件 + MCP + Trakt + OIDC + 自定义命令,本批):
    • Plex 套件(内置插件 ptp-plex-suite):新子包 internal/plugins/plexsuite/(自包含 Plex REST 客户端,X-Plex-Token 头,内网 AllowPrivate)实现 4 特性——库本地化(PUT /:/prefs?Language=)+ 跳片头分析触发(GET /library/sections/{id}/analyze,需 Plex Pass 标 TODO)+ 自动音频/字幕语言(/:/prefs?AudioStreamLanguage=&SubtitleStreamLanguage=)+ IP 优选(并发探测候选地址取最快写回 server_url);凭据由插件配置承载不耦合 mediaserver 表;Schedule 每日 4 点 + actions apply/test/ip_optimize。特性 pref 键名标 TODO 联调(httptest mock 覆盖探测/排序)。
    • MCP Server(internal/mcp/ + 插件 ptp-mcp-server):JSON-RPC 2.0 over streamable HTTP 核心子集(initialize/ping/tools/list/tools/call/notifications/initialized),纯标准库;公开路由 /api/v1/mcp(GET/POST,由 Server 自身校验 enabled + Bearer token);注册 5 只读宿主工具(system.info/sites.list/subscriptions.list/downloaders.list/torrents.search,密钥字段 model json:"-" 不泄露);配置(启用/令牌/基地址)由插件承载,Server 经 SettingsGetter 每请求读插件配置(单一真源热改)。工具错误以 isError=true content 返回不破坏 JSON-RPC 信封;notifications 返 202 不回包。
    • Trakt 同步(chain,需 SubscribeChain):新包 internal/trakt/(Device OAuth:/oauth/device/code + /oauth/device/token 轮询 + authorization_pending/slow_down DeviceTokenError 不按 400 失败 + RefreshToken + /users/me/watchlist/{movies,shows} 解析 tmdb/imdb/trakt ids)+ TraktChain(StartDeviceAuth→PollDeviceStatus→refreshTokenIfNeeded→SyncWatchlist 映射为 Subscription,subCh.Create 幂等去重;settings 走 traktStore 接口便无 DB 单测);API /trakt/*(config GET/PUT 脱敏 + device-code/device-status/sync/history)+ 公开 /trakt/device HTML 引导页(JS 自动轮询);cron 每小时同步。三端 Trakt 页(设备授权引导 + 同步状态 + 配置)+ glyph trakt(播放列表)粉红渐变两端 1:1。
    • OIDC/SSO(internal/auth/oidc.go + 路由):通用 OIDC Authorization Code + PKCE(S256) 兼容 Authelia/Keycloak/Casdoor;discovery /.well-known/openid-configuration 10min 缓存;公开 /auth/oidc/start(state+verifier cookie + 302)+ /auth/oidc/callback(state 校验 + 换令牌 + userinfo/id_token fallback + upsert User oidc:<sub> + 签 JWT + 302 回前端 ?oidc_token=)+ 管理员 /auth/oidc/config GET/PUT;/auth/status 加 oidc_enabled/oidc_display_name 驱动登录页入口。用 SettingsStore 接口破 auth→oper→monitor→auth 循环(同 notice.Store);OIDC 由管理员启用视为可信身份(VipLevel=1 放行 VIP 准入,首个 OIDC 用户为 admin)。三端登录页加 OIDC 按钮(Web 整页跳转 + 回调读 ?oidc_token 存 token;Flutter url_launcher 开浏览器,deep-link 取回令牌留 TODO)+ Web 设置页 OIDC 配置 Tab。
    • 自定义命令(内置插件 ptp-custom-command):定时在宿主机执行一组 shell 命令并按退出码推送(name|command[|timeout] 每行 + 全局 cron + 成功/失败推送策略 + sh -c + 超时 kill);与工作流 ssh_command(agent 远程)互补,本插件面向宿主机本机;actions run/test + Schedule 每 6 小时。单测覆盖 parseCmdItems/atoiOr + runOne 成功/失败/超时 + 推送策略。
    • 验证全绿:go build + vet + test(mcp initialize/tools/error/notif + 401/404 门禁;trakt device code/pending(400)/success/watchlist httptest;plexsuite 探测排序;custom_command 超时/失败;trakt chain settings 往返)+ vue-tsc + npm build(dist 重建嵌入)+ dart analyze No issues + flutter build windows ✓。真实 Plex pref 键名 / Trakt Device 页 / OIDC Provider 标 TODO 联调(mock 覆盖)。

17 功能 7 批全部完成 ✅(P0–P3 全做、三端对齐、编译+单测+mock 验证;真实 PT/Emby/Plex/网盘/OIDC Provider 联调留用户)。

Web 导航微调:搜索移出侧栏 → 顶栏图标(2026-07-22) ​

主菜单「内容获取」组移除「搜索」项;搜索改为顶栏常驻图标(放大镜,位于折叠导航按钮右侧),点击直达 /search,命中当前页时与侧栏选中态一致高亮(.icon-btn.router-link-active 紫蓝渐变)。

  • 动机:搜索是横向跳转能力而非具体任务域,放顶栏更贴合「主菜单只回答我今天做什么」,侧栏瘦身。设计稿 menu-redesign.md §3 裁决同步由「A + 顶栏 双入口」收敛为「仅顶栏」(顶部加 2026-07-22 修订记录、§4 定稿菜单树删除搜索行)。
  • 改动集中 MainLayout.vue:navGroups「内容获取」组删 search 项 + import Search 图标 + topbar 折叠导航按钮后插 <router-link :to="{ name: 'search' }"> + 加 .icon-btn.router-link-active 高亮。
  • 验证:Search 为 @element-plus/icons-vue 标准导出(dist/types/components/index.d.ts:225 实查),search 路由早已存在(router/index.ts:25);本次仅模板 / CSS / 标准图标 import,未改逻辑与路由表。