feat(qbittorrent): login compat for v4/v5.0-5.2 and three auth modes

- Login success = HTTP 200 "Ok." or 204 (5.2+); session cookie taken from
  Set-Cookie by name (SID / QBT_SID_<port> / custom) and sent back as-is.
- Failure classes: "Fails." / 401 -> auth; 403 IP ban / 429 -> rate;
  wrong API Key (403) -> auth with hint; 5xx -> protocol.
- Downloader instance auth kind: password / api_key (qB 5.2+ Bearer, no
  /auth/login) / none (whitelisted subnet); stored in settings.authKind,
  credential write-only in secret_ref, cleared when switching to none.
  One qbAuthConfig() shared by sync, agent capability and connection test.
- Settings form: auth kind select, password / API Key fields (no echo).
- testkit: mock qB 5.2 login behaviour, IP ban, bypassAuth;
  makeTorrent({ realPieces }) builds real piece hashes for real qB checks.
- Real-container matrix (QB_REAL, skipped by default): 5.1.2 and 5.2.4,
  login / add / query / tags / pause-resume / delete keeping data.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:42:17 +08:00
1 parent c846e91cc4
commit 74f1566985
14 files changed
+756 -59

No files matched your search

+46 -6
View File
@@ -67,6 +67,19 @@ export function TestButton({
const credentialHint = { editHint: "留空不修改" };
const AUTH_KINDS = [
{ value: "password", label: "用户名和密码" },
{ value: "api_key", label: "API Key" },
{ value: "none", label: "无认证(内网白名单)" },
];
/** 已存认证方式;旧数据没有 settings.authKind 时按用户名 / 凭证推断(与服务端一致) */
function authKindOf(r: DownloaderInstance): string {
const k = (r.settings as { authKind?: unknown }).authKind;
if (k === "password" || k === "api_key" || k === "none") return k;
return r.username ? "password" : r.secretRef ? "api_key" : "none";
}
export function DownloadersSection() {
return (
<SettingsBlock section="downloaders" anchor="instances" title="qBittorrent 实例" plain>
@@ -121,8 +134,29 @@ function DownloadersTable() {
required: true,
placeholder: "http://192.168.1.10:8080",
},
{ name: "username", label: "用户名", kind: "text", hint: "留空时凭证按 WebUI API Key 使用" },
{ name: "credential", label: "密码 / API Key", kind: "password", ...credentialHint },
{
name: "authKind",
label: "认证方式",
kind: "select",
options: AUTH_KINDS,
hint: "API Key 需要 qBittorrent 5.2 及以上,在 WebUI 设置里生成",
},
{ name: "username", label: "用户名", kind: "text", visible: (v) => v.authKind === "password" },
{
name: "password",
label: "密码",
kind: "password",
visible: (v) => v.authKind === "password",
...credentialHint,
},
{
name: "apiKey",
label: "API Key",
kind: "password",
placeholder: "qbt_…",
visible: (v) => v.authKind === "api_key",
...credentialHint,
},
{ name: "managedTag", label: "管理标签", kind: "text", placeholder: "umt", hint: "只接管带此标签的任务" },
{ name: "cacheVolumeKey", label: "缓存卷标识", kind: "text", placeholder: "cache-1" },
{ name: "enabled", label: "启用", kind: "switch" },
@@ -130,8 +164,10 @@ function DownloadersTable() {
initial: {
name: "",
baseUrl: "",
authKind: "password",
username: "",
credential: "",
password: "",
apiKey: "",
managedTag: "umt",
cacheVolumeKey: "",
enabled: true,
@@ -139,8 +175,10 @@ function DownloadersTable() {
fromRow: (r) => ({
name: r.name,
baseUrl: r.baseUrl,
authKind: authKindOf(r),
username: r.username ?? "",
credential: "",
password: "",
apiKey: "",
managedTag: r.managedTag,
cacheVolumeKey: r.cacheVolumeKey ?? "",
enabled: r.enabled,
@@ -149,8 +187,10 @@ function DownloadersTable() {
name: str(v.name),
kind: mode === "create" ? "qbittorrent" : undefined,
baseUrl: str(v.baseUrl),
username: nullStr(v.username),
credential: optStr(v.credential),
authKind: str(v.authKind),
username: v.authKind === "password" ? nullStr(v.username) : null,
credential:
v.authKind === "password" ? optStr(v.password) : v.authKind === "api_key" ? optStr(v.apiKey) : undefined,
managedTag: optStr(v.managedTag),
cacheVolumeKey: nullStr(v.cacheVolumeKey),
enabled: v.enabled === true,