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]>
10 KiB
站点运行手册(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)→ GETindex.php,loginSuccess(缺省a[href*="logout"])命中即成功。Cookie 存session_secret_ref(purposesite_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 模块,留给该模块的负责人。
待核验
- takelogin 表单字段、二次验证字段名、错误文本全部只对
@umt/testkitmock 站点验证过,未连接真实站点。 - 失败文本归类是关键词匹配(
classifyLoginError),真实站点的措辞可能落到unknown(状态记为 expired,原文截 200 字写进last_error)。 - Cloudflare 站点只识别并转待处理,不做浏览器登录。
- 馒头用户数据:
MTorrent解析器的做种列表首页按 GET 请求,真实 API 可能要求 POST。