Files
UHD_Media_Tool/docs/runbook/sites.md
T
rootandClaude Opus 5.5 4a92dfd2dc feat(feeds): site-scoped RSS add infers account, parser and poll
Adding a feed from a site page now only asks for the URL, purpose
(discover / track / promotion) and an optional note. POST /feed-sources
with siteId infers the site account (passkey holder first), records the
site definition version as parserVersion, defaults the name to
"<site> RSS" and polling to 30 minutes, and returns non-blocking
warnings when the URL host does not match the site. The dialog shows the
redacted URL, fetches once right after saving and reports the item count
or the error.

The generic feed form on the sources page suggests linking a registered
site when the URL host matches (can be undone). name is now optional.

Takes effect after the stack is rebuilt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 14:43:19 +08:00

10 KiB
Raw Blame History

站点运行手册(W9-A 站点中心后端)

规格:docs/design/site-center.md。实现:apps/server/src/modules/sources-identity/service/account.ts(凭证、刷新登录、用户数据、健康检测、删除、定时任务)、routes/sources.ts、runtime.ts(凭证注入、站点默认、代理);账密登录 packages/adapters/src/sites/login/nexusphp.ts;迁移 0031_site_center。

端点

端点 scope 说明
GET /sites?include=accounts read 列表;带 include=accounts 时每项附 accounts[](完整 SiteAccount)和 accountStatus(多账号取最差,无启用账号为 unconfigured)。已删除站点不出现
POST /sites / PATCH /sites/{id} admin 登记 / 修改;新增 proxy。rateLimit 为空、snapshotIntervalSec 为 null 时继承站点默认
DELETE /sites/{id} admin 见"删除"
PUT /sites/{id}/credentials admin 按 kind 判别:password{username,password,totpSecret?,saveCookieOnly?,userAgent?} / cookie{cookie,userAgent?} / api_key{apiKey} / passkey{passkey},都带 label(缺省 default)。旧格式 {authKind, credential, username?} 仍接受。明文进 secret 表,响应只有 secretRef
POST /sites/{id}/refresh-login admin body {accountId?, wait?}。缺省异步入队 refreshSiteAccount 返回 {taskId, result:null}(幂等键按 10 s 取整);wait:true 同步执行(≤30 s)返回 {taskId:null, result: SiteLoginResult}
POST /sites/{id}/health-check admin 同步(≤20 s,走站点限流);返回 SiteHealthResult,写 site.last_health、last_health_at、health_status
GET /sites/{id}/user-data?accountId&days=30 read site_user_data 按天历史(1–400 天)
DELETE /site-accounts/{id} admin 删账号及其全部 secret

POST/PATCH /site-accounts 的凭证部分已标弃用,新客户端只用 PUT /sites/{id}/credentials。该端点从 auth-settings 模块迁到 sources-identity。

账号状态(site_account.status)

值 界面 进入条件
unknown 未验证 新建或更新凭证
active 正常 刷新登录 / 校验成功
expired 已失效 Cookie 校验失败;账密登录后仍未检测到登录态;快照 / Feed 遇 auth_expired
invalid 已失效 密码错、账号封禁 / 未确认、API Key 无效
challenge 需验证 验证码、二次验证(无 TOTP)、Cloudflare 挑战

网络失败只写 last_error(reason:"network"),状态不变。站点没有定义、Passkey 账号、不支持账密的站点(馒头等)返回 reason:"unsupported",状态不变。

失败时开待处理事项:kind source_unreachable,dedupeKey = site-login:<accountId>,subject site_account。选项 use_cookie(input 带 siteId / accountId,前端深链到凭证对话框)、retry(仅 challenge;选中后入队重新登录)、dismiss。更新凭证或登录成功时关闭该事项和 site-auth:<siteId>。

刷新登录流程

  • 账密(仅 umt.kind = nexusphp 且定义有 umt.login):GET 登录页 → 页面有 imagecode.php 判验证码 → 取 takelogin 表单隐藏字段,填用户名 / 密码 / selectDefaults / extraFormData,有 TOTP 且表单有 two_step_code/otp 等字段时填当前 6 位码 → POST(不跟随 3xx)→ GET index.php,loginSuccess(缺省 a[href*="logout"])命中即成功。Cookie 存 session_secret_ref(purpose site_cookie),之后适配器取 cookie 时优先用它。saveCookieOnly 为真时,成功后凭证类型改为 cookie,密码与 TOTP secret 删除。
  • Cookie / API Key:用 refreshSiteUserData 取用户数据作为校验(不做 schema 回退,避免对站点连发 20 次请求);站点没有解析器时退回适配器 healthCheck()。
  • 成功后写 user_data、user_data_at,并 upsert 当日 site_user_data(同日取最新)。账密登录成功但用户数据没取到时仍算登录成功,message 说明原因。
  • 快照 / Feed 遇登录失效(onAuthError)时,若站点有账密账号,入队一次自动重登(同账号 10 分钟一个幂等槽)。

用户数据字段

SiteAccount.userData:schema, userId, username, userLevel, vip, joinAt, upload, download, ratio, bonus, seeding, seedingSize, leeching, leechingSize, messageUnread。ratio 为 null 表示站点没给且下载量为 0;bonus 为 0 时记 null(不显示);vip 由定义 umt.profile.vip_css(首页命中)或 umt.vipLevels(等级名包含匹配)判定,都没配置为 null。做种分页上限取 settings.sites.userDataMaxSeedingPages。

健康检测

healthCheck() 结果映射:ok → healthy,auth_* → failing,其余(限流、不可达、维护、解析失败)→ degraded;已关闭站点保持 disabled。loggedIn:ok 为 true,auth_* 为 false,其余 null。首页已登录时顺手更新 active 账号的 last_login_at。

删除

DELETE /sites/{id} 在一个事务里:资源 availability 改为 site_disabled(资源和下载任务都保留,source_release.site_id 外键为 restrict)、Feed 停用并解除账号关联、关闭该站待处理、删全部账号(site_user_data 随账号级联删除)、站点行软删除(deleted_at,enabled=false,key 改为 <key>-deleted-<时间>,同一定义可以重新登记)。事务提交后删除账号的全部 secret。

Feed(RSS)添加

  • 站点内添加(站点详情「快照与 Feed」→ 新建 Feed,components/sites/site-feed-dialog.tsx):只填地址、用途(发现 = generic / 追踪 = search / 促销 = promotion,缺省发现)、可选备注(即名称)。地址输入时下方预览脱敏后的显示值;域名与站点不符给黄色提示但允许保存。保存后立即 POST /feed-sources/{id}/fetch 一次,对话框每 2 s 轮询 GET /feed-sources/{id}(最多 90 s),显示首抓条目数(游标窗口 lastGuidSet 长度)或 lastError。
  • POST /feed-sources 带 siteId 时的推断(service/feeds.ts feedDefaultsForSite):siteAccountId 缺省取该站启用账号,优先带 passkey 的,再取最早建的;parserVersion 记为 rss@1;<站点定义 umt.version>(解析仍走通用 RSS 解析器,版本只作追溯);name 缺省「站点名 RSS」;pollIntervalSec 缺省 1800(站点请求克制;不挂站点的 Feed 仍缺省 900)。地址主机键(去 www.、取最后两级,IP 原样)与站点 baseUrl / 定义 domain 都不一致时,响应附 warnings[](只在创建响应出现)。name 现在可省,不挂站点时缺省为地址主机名。
  • 来源页任意 RSS(components/workbench/feed-form.tsx):完整表单不变;新建时地址域名匹配已登记站点会自动选中该站和账号并提示「按地址域名关联到 X · 不关联」,手动改过站点后不再自动改。

定时任务

key 名称 默认 行为
sites.user_data 站点数据刷新 每 6 小时,一个周期后首跑 启用站点里 status=active 的非 passkey 账号逐个入队 refreshSiteAccount{mode:"userData"};会话失效的账密账号自动重登一次;只读对账模式跳过
sites.health_check 站点健康检测 每小时,一个周期后首跑 启用站点(非 generic_rss)逐个同步检测;可在定时任务页关闭

注:定时任务 key 只允许小写(注册表校验),所以规格里的 sites.userData / sites.healthCheck 落为下划线形式。

站点默认(settings.sites)

字段 缺省 说明
defaultRateLimit {requestsPerMinute: 20, concurrency: 1} 站点 rate_limit 为空时使用;不会比定义里的 umt.rateLimit 更激进(取两者较慢的间隔、较小的并发)
defaultSnapshotIntervalSec 3600 站点开了定时快照但未设周期时使用
userAgent 固定桌面 Chrome UA 账号未设 UA 时用于登录与用户数据
proxyUrl null 站点 proxy=true 时,适配器、登录、用户数据请求都经此代理(Deno.createHttpClient)
userDataMaxSeedingPages 10 用户数据做种分页上限

运行时每 30 s 最多重读一次设置;设置变化会让站点适配器按新签名重建。

Agent 能力(W10-pre2)

处理器在 apps/server/src/modules/sources-identity/capabilities-sites.ts,调用上面同一组 service。能力 ID 只允许小写 + 下划线(CapabilityId 正则),所以规格里的 site.healthCheck / site.refreshLogin / site.userData 落为下划线形式。

能力 类别 风险 重放 HTTP scope 说明
site.list read low safe read 站点健康、lastHealth、整体账号状态与账号摘要;缺省只列启用站点(includeDisabled、keys 可选)
site.health_check execute low safe admin 同 POST /sites/{id}/health-check;幂等,只读首页并写回健康列,所以例外地声明 replay safe
site.refresh_login execute medium never admin 同步执行 refreshSiteAccount(mode=login),上限 30 s(同时跟随任务取消);accountId 缺省取第一个启用的非 passkey 账号
site.user_data read low safe read 各账号最近一次用户数据 + site_user_data 按天历史(days 缺省 30,最多 400)
  • 授权:两个 execute 能力走通用授权,Agent / API Key / 后台调用必须有 automation_grant(action 为能力 ID 或 *),自动化总开关关闭或只读对账时一律拒绝;控制台管理员从 Web 直接调用视为人工授权。没有计划可绑定,所以不走单次审批。
  • 脱敏:输出按白名单逐字段拼装,没有 secretRef / sessionSecretRef / passkeySecretRef、Cookie、密码、登录名、UA 与站点 config;凭证只给 hasSession / hasPasskey 两个布尔。测试 apps/server/test/sources-identity/site-capabilities.test.ts 逐项断言。
  • 注册数:首批能力 17 → 21,启动断言 assertCapabilityCount 跟随 CAPABILITY_IDS.length。
  • 未接:agent/invoke-limits.ts 的站点并发上限还没把 site.health_check / site.refresh_login 算作外部站点调用;Agent 默认只读白名单(agent/runner.ts)也还没加 site.list / site.user_data。两处都在 agent 模块,留给该模块的负责人。

待核验

  1. takelogin 表单字段、二次验证字段名、错误文本全部只对 @umt/testkit mock 站点验证过,未连接真实站点。
  2. 失败文本归类是关键词匹配(classifyLoginError),真实站点的措辞可能落到 unknown(状态记为 expired,原文截 200 字写进 last_error)。
  3. Cloudflare 站点只识别并转待处理,不做浏览器登录。
  4. 馒头用户数据:MTorrent 解析器的做种列表首页按 GET 请求,真实 API 可能要求 POST。