fix(agent): download.recheck 复用下载同步的 qB 客户端

qB 客户端缓存改为按 ServerContext 共享(downloaderClientFor),Agent 不再自己 new 客户端,
与下载同步、面板共用同一登录会话和凭证处理。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 23:20:28 +08:00
1 parent 74f2c7649e
commit 3d3a4cefc6
2 files changed
+35 -28

No files matched your search

@@ -117,12 +117,42 @@ export const DEFAULT_OPTIONS: AoOptions = {
const str = (v: unknown) => (typeof v === "string" && v.length > 0 ? v : undefined);
/** qB 客户端按 ServerContext 共享缓存(实例配置变了重建):下载同步、面板、Agent 的 download.recheck 共用同一登录会话 */
const sharedClients = new WeakMap<ServerContext, Map<string, { stamp: string; client: DownloaderClient }>>();
/** 按实例取 qB 客户端(凭证现场经 secrets 解出,解析失败按无凭证处理并记警告) */
export async function downloaderClientFor(ctx: ServerContext, inst: DownloaderInstanceRow): Promise<DownloaderClient> {
let clients = sharedClients.get(ctx);
if (!clients) {
clients = new Map();
sharedClients.set(ctx, clients);
}
const stamp = `${inst.updated_at.toISOString()}|${inst.base_url}`;
const hit = clients.get(inst.id);
if (hit && hit.stamp === stamp) return hit.client;
const settings = (inst.settings ?? {}) as Record<string, unknown>;
let secret: string | undefined;
if (inst.secret_ref) {
try {
secret = await ctx.secrets.resolve(inst.secret_ref);
} catch (err) {
ctx.logger.warn({ err: { message: (err as Error).message } }, "凭证解析失败");
}
}
const client = createQbClient({
instanceId: inst.id,
baseUrl: inst.base_url,
...qbAuthConfig({ authKind: settings.authKind, username: inst.username, secret: secret ?? null }),
});
clients.set(inst.id, { stamp, client });
return client;
}
/** 用 ctx 组装默认端口;overrides 逐项覆盖(测试) */
export function createPorts(
ctx: ServerContext,
overrides: Partial<Omit<AoPorts, "options">> & { options?: Partial<AoOptions> } = {},
): AoPorts {
const clients = new Map<string, { stamp: string; client: DownloaderClient }>();
const resolveSecret = async (ref: string | null): Promise<string | undefined> => {
if (!ref) return undefined;
try {
@@ -159,20 +189,7 @@ export function createPorts(
if (r.created) await enqueueNewEntityRefresh(ctx, r.mediaEntityId);
return r.mediaEntityId;
},
async downloader(inst) {
const stamp = `${inst.updated_at.toISOString()}|${inst.base_url}`;
const hit = clients.get(inst.id);
if (hit && hit.stamp === stamp) return hit.client;
const settings = (inst.settings ?? {}) as Record<string, unknown>;
const secret = await resolveSecret(inst.secret_ref);
const client = createQbClient({
instanceId: inst.id,
baseUrl: inst.base_url,
...qbAuthConfig({ authKind: settings.authKind, username: inst.username, secret: secret ?? null }),
});
clients.set(inst.id, { stamp, client });
return client;
},
downloader: (inst) => downloaderClientFor(ctx, inst),
async torrentSource({ release, site }) {
// 永久性条件用 SiteError:submitDownload 据此把计划判失败并释放 Slot,而不是反复重试、Slot 一直 reserved
if (!site) throw new SiteError("", "not_found", "站点不存在,无法获取种子");