From 51c31963118ef11573e348b37fc4f68634328a25 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 15:18:40 +0800 Subject: [PATCH] docs(runbook): sites (site center endpoints, login states, jobs, defaults) Co-Authored-By: Claude Opus 5.5 --- docs/runbook/sites.md | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/runbook/sites.md diff --git a/docs/runbook/sites.md b/docs/runbook/sites.md new file mode 100644 index 00000000..2d26d4dc --- /dev/null +++ b/docs/runbook/sites.md @@ -0,0 +1,79 @@ +# 站点运行手册(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:`,subject `site_account`。选项 `use_cookie`(input 带 siteId / accountId,前端深链到凭证对话框)、`retry`(仅 challenge;选中后入队重新登录)、`dismiss`。更新凭证或登录成功时关闭该事项和 `site-auth:`。 + +## 刷新登录流程 + +- **账密**(仅 `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 改为 `-deleted-<时间>`,同一定义可以重新登记)。事务提交后删除账号的全部 secret。 + +## 定时任务 + +| 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 最多重读一次设置;设置变化会让站点适配器按新签名重建。 + +## 待核验 + +1. takelogin 表单字段、二次验证字段名、错误文本全部只对 `@umt/testkit` mock 站点验证过,未连接真实站点。 +2. 失败文本归类是关键词匹配(`classifyLoginError`),真实站点的措辞可能落到 `unknown`(状态记为 expired,原文截 200 字写进 `last_error`)。 +3. Cloudflare 站点只识别并转待处理,不做浏览器登录。 +4. 馒头用户数据:`MTorrent` 解析器的做种列表首页按 GET 请求,真实 API 可能要求 POST。