蓝影论坛 Logo
22浏览量
1参与者
活动 102天
✨这是 parkertaylor 首次发帖 - 让我们欢迎他/她加入社区吧!
parkertaylor
楼主
## 概述 这是一个 Vertex 定时脚本,用来管理 Vertex 中所有运行中下载器客户端里的 `sky` 分类种子。可实现空的自动刷流管理,最好配合错误种子重启脚本试用。 - 前1小时限速,1小时后放开限速。 - 空间不足时,按照最近平均速度、添加时间、体积等排序清理种子。 - 清理长时间无上传的种子。 ~~~ async () => { const moment = require('moment') const util = require('../libs/util') const clients = global.runningClient || {} const BASE_CONFIG = { category: 'sky', // 分类 debug: true // 调试日志开关,稳定后可改为 false } const RATIO_CONFIG = { resumeDelay: 58 * 60, // 初始等待时长 (1小时) maxRatio: 3.1 // 目标分享率 (扣除初始量) } const SPACE_CLEANUP_CONFIG = { minFreeSpace: 25 * 1024 * 1024 * 1024, // 触发清理的空间阈值 targetFreeSpace: 30 * 1024 * 1024 * 1024, // 清理目标空间 panicSpace: 5 * 1024 * 1024 * 1024, // 恐慌空间 (无视大部分保护) protectUploadSpeed: 10 * 1024 * 1024, // 优质未完成种保护阈值 uploadSpeedPriorityDiff: 10 * 1024, // 上传速度差超过10KB/s时,按速度排序 postLimitGrace: 10 * 60, // 解除初始限速后的观察时间 slowWindow: 10 * 60, // 持续低上传统计窗口 slowUploadSpeed: 1 * 1024 * 1024, // 持续低上传阈值 stuckProgress: 0.995, // 近完成低效种进度阈值 stuckDownloadSpeed: 128 * 1024, // 近完成低效种下载速度阈值 addedTimePriorityDiff: 30 * 60, // 添加时间差超过30分钟时,优先删更早添加的 ratioPriorityDiff: 0.1 // 有效Ratio差超过0.1时,优先删更低的 } const SPEED_LIMIT_CONFIG = { initialUploadLimit: 1024, // 初始限速 (KB/s),0 为不限速,负数为暂停 standardUploadLimit: 460 * 1024 // 标准限速 (KB/s),0 为无限制 } const IDLE_CLEANUP_CONFIG = { enabled: true, // 是否开启空闲种主动清理,默认开启 idleWindow: 30 * 60, // 最近多少秒内上传/下载都无增量才算空闲 minAge: 90 * 60, // 添加满 90 分钟才允许空闲清理 requireStandardLimit: true, // 必须已经进入标准限速阶段,避免误删新种 requireCurrentSpeedZero: true, // 当前上传/下载速度必须为 0 requireFlowRecord: true, // 最近窗口必须有流量记录;没有记录不当作空闲 minEffectiveRatio: 0, // 有效 Ratio 低于该值时不做空闲清理 maxDeletePerRun: 0 // 每轮最多删多少空闲种;0 表示不限制 } // 保留统一 CONFIG,方便下面逻辑复用,同时让用户按功能块看配置。 const CONFIG = { ...BASE_CONFIG, ...RATIO_CONFIG, ...SPACE_CLEANUP_CONFIG, ...SPEED_LIMIT_CONFIG, idleCleanup: IDLE_CLEANUP_CONFIG } const SCRIPT_NAME = '空控速删种' const formatError = (error) => { if (!error) return '未知错误' return error.stack || error.message || String(error) } const debugLog = (message) => { if (CONFIG.debug) { logger.info(`[${SCRIPT_NAME}][DEBUG] ${message}`) } } const getTorrentList = (torrents) => { if (Array.isArray(torrents)) return torrents if (torrents && typeof torrents === 'object') return Object.values(torrents) return [] } const getAddedTime = (torrent) => { return Number(torrent.addedTime ?? torrent.added_on ?? torrent.addedTimeStamp ?? 0) } const getCurrentUpLimit = (torrent) => { return Number(torrent.originProp?.up_limit ?? torrent.up_limit ?? 0) } // 判定种子是否被站点删除/封禁 const checkIsInvalid = (torrent) => { const trackerMessage = [ torrent.trackerStatus, torrent.tracker_status, torrent.message, torrent.error ].filter(Boolean).join(' ').toLowerCase() if (!trackerMessage) return false const deletedMessages = [ "torrent banned", "Torrent not exists", "torrent not registered with this tracker", "unregistered torrent", "Invalid Torrent:" ] return deletedMessages.some(msg => trackerMessage.includes(msg.toLowerCase())) } // 获取扣除等待期上传量后的统计数据 const getEffectiveStats = async (torrent) => { const addedTime = getAddedTime(torrent) if (!torrent.hash || !addedTime) return { effectiveUpload: 0, effectiveRatio: 0 } const boundaryTime = addedTime + CONFIG.resumeDelay const now = moment().unix() if (now <= boundaryTime) return { effectiveUpload: 0, effectiveRatio: 0 } const record = await util.getRecord( 'SELECT upload FROM torrent_flow WHERE hash = ? AND time >= ? ORDER BY time ASC LIMIT 1', [torrent.hash, boundaryTime] ) const uploadAtBoundary = record ? Number(record.upload) || 0 : 0 const size = Number(torrent.size) || 0 const effectiveUpload = Math.max(0, (Number(torrent.uploaded) || 0) - uploadAtBoundary) const effectiveRatio = size > 0 ? (effectiveUpload / size) : 0 return { effectiveUpload, effectiveRatio } } const getRecentUploadSpeed = async (torrent, now) => { const windowStart = now - CONFIG.slowWindow const record = await util.getRecord( 'SELECT upload, time FROM torrent_flow WHERE hash = ? AND time >= ? ORDER BY time ASC LIMIT 1', [torrent.hash, windowStart] ) if (!record || record.upload === undefined || record.time === undefined) { return torrent.upspeed || 0 } const elapsed = Math.max(1, now - Number(record.time)) const uploadDiff = Math.max(0, (torrent.uploaded || 0) - (Number(record.upload) || 0)) return uploadDiff / elapsed } // 空闲清理是主动腾空间功能:不等空间告急,只清理已过保护期且窗口内完全无流量的种子。 const checkIsIdleForCleanup = async (torrent, now) => { const idleConfig = CONFIG.idleCleanup const addedTime = getAddedTime(torrent) if (!idleConfig.enabled) return { matched: false, reason: '空闲清理未开启' } if (torrent.__skyDeleted) return { matched: false, reason: '本轮已删除' } if (!torrent.hash) return { matched: false, reason: '缺少 hash' } if (!addedTime) return { matched: false, reason: '缺少添加时间' } if (now - addedTime < idleConfig.minAge) return { matched: false, reason: '未达到空闲清理最小年龄' } if (!hasExitedProtection(torrent)) return { matched: false, reason: '仍在保护/观察期' } if ((torrent.effectiveRatio || 0) < idleConfig.minEffectiveRatio) return { matched: false, reason: '有效 Ratio 未达空闲清理阈值' } if (idleConfig.requireStandardLimit) { const standardLimitByte = CONFIG.standardUploadLimit * 1024 if (getCurrentUpLimit(torrent) !== standardLimitByte) { return { matched: false, reason: '未处于标准限速' } } } if (idleConfig.requireCurrentSpeedZero && ((torrent.upspeed || 0) !== 0 || (torrent.dlspeed || 0) !== 0)) { return { matched: false, reason: '当前仍有上传/下载速度' } } if (torrent.uploaded === undefined || torrent.downloaded === undefined) { return { matched: false, reason: '缺少上传/下载统计字段' } } const windowStart = now - idleConfig.idleWindow const record = await util.getRecord( 'SELECT upload, download, time FROM torrent_flow WHERE hash = ? AND time >= ? ORDER BY time ASC LIMIT 1', [torrent.hash, windowStart] ) if (!record) { return idleConfig.requireFlowRecord ? { matched: false, reason: '空闲窗口缺少流量记录' } : { matched: true, reason: `最近${Math.floor(idleConfig.idleWindow / 60)}分钟无流量记录` } } if (record.upload === undefined || record.download === undefined) { return { matched: false, reason: '空闲窗口记录缺少上传/下载字段' } } const uploadDiff = Math.max(0, (Number(torrent.uploaded) || 0) - (Number(record.upload) || 0)) const downloadDiff = Math.max(0, (Number(torrent.downloaded) || 0) - (Number(record.download) || 0)) if (uploadDiff !== 0 || downloadDiff !== 0) { return { matched: false, reason: `窗口内仍有流量 上传${formatSize(uploadDiff)} 下载${formatSize(downloadDiff)}` } } return { matched: true, reason: `持续空闲${Math.floor(idleConfig.idleWindow / 60)}分钟` } } const formatSize = (bytes) => { const units = ['B', 'KB', 'MB', 'GB', 'TB'] let value = Number(bytes) || 0 let index = 0 while (value >= 1024 && index < units.length - 1) { value /= 1024 index++ } return `${value.toFixed(index === 0 ? 0 : 2)}${units[index]}` } const formatSpeed = (bytes) => `${formatSize(bytes)}/s` const isHighValueActive = (torrent) => { return (torrent.progress || 0) < 1 && (torrent.upspeed || 0) > CONFIG.protectUploadSpeed } const hasExitedProtection = (torrent) => { return (torrent.timeActive || 0) >= CONFIG.resumeDelay + CONFIG.postLimitGrace } const isNearCompleteSlow = (torrent) => { const progress = torrent.progress || 0 return progress >= CONFIG.stuckProgress && progress < 1 && hasExitedProtection(torrent) && (torrent.recentUploadSpeed || 0) < CONFIG.slowUploadSpeed && (torrent.dlspeed || 0) < CONFIG.stuckDownloadSpeed } const isSustainedSlowUpload = (torrent) => { return hasExitedProtection(torrent) && (torrent.recentUploadSpeed || 0) < CONFIG.slowUploadSpeed } const CLEANUP_TAG_PRIORITY = { invalid: 0, stuckNearComplete: 1, sustainedSlowUpload: 2, normal: 3, highValueFallback: 4 } const CLEANUP_TAG_LABEL = { invalid: '站点失效', stuckNearComplete: '近完成低效种', sustainedSlowUpload: '持续低上传种', normal: '普通候选', highValueFallback: '优质活跃种兜底' } const getCleanupTag = (torrent) => { if (torrent.isInvalid) return 'invalid' if (isHighValueActive(torrent)) return 'highValueFallback' if (isNearCompleteSlow(torrent)) return 'stuckNearComplete' if (isSustainedSlowUpload(torrent)) return 'sustainedSlowUpload' return 'normal' } const buildCleanupReason = (torrent, isPanicMode) => { const reasons = [isPanicMode ? '恐慌清理' : '空间清理'] const state = torrent.state || '' if (torrent.cleanupTag) reasons.push(CLEANUP_TAG_LABEL[torrent.cleanupTag] || torrent.cleanupTag) if (torrent.isInvalid) reasons.push('站点失效') if (state.toLowerCase().includes('error')) reasons.push('状态报错') reasons.push(`进度${((torrent.progress || 0) * 100).toFixed(1)}%`) reasons.push(`添加${Math.floor((moment().unix() - getAddedTime(torrent)) / 60)}分钟前`) reasons.push(`当前上传${formatSpeed(torrent.upspeed || 0)}`) reasons.push(`10分钟均速${formatSpeed(torrent.recentUploadSpeed || 0)}`) reasons.push(`当前下载${formatSpeed(torrent.dlspeed || 0)}`) reasons.push(`有效Ratio${torrent.effectiveRatio.toFixed(3)}`) reasons.push(`占用${formatSize(torrent.actualSize || 0)}`) return reasons.join(' / ') } try { const clientIds = Object.keys(clients) debugLog(`开始运行,下载器数量: ${clientIds.length}`) if (clientIds.length === 0) { logger.error(`[${SCRIPT_NAME}] 未找到正在运行的下载器`) return } for (const clientId of clientIds) { try { const client = clients[clientId] const maindata = client && client.maindata const serverState = maindata && (maindata.serverState || maindata.server_state) if (!client || !maindata || !maindata.torrents || !serverState) { logger.error(`[${SCRIPT_NAME}] [${clientId}] 下载器信息不完整,跳过`) continue } const torrentsRaw = maindata.torrents const torrents = getTorrentList(torrentsRaw) const now = moment().unix() const currentFreeSpace = Number(serverState.free_space_on_disk || 0) const isPanic = currentFreeSpace < CONFIG.panicSpace const torrentsType = Array.isArray(torrentsRaw) ? 'array' : typeof torrentsRaw debugLog(`[${clientId}] torrents 类型: ${torrentsType},总数: ${torrents.length},剩余空间: ${formatSize(currentFreeSpace)},恐慌模式: ${isPanic ? '是' : '否'}`) // 筛选并预计算 const skyTorrents = [] let missingAddedTime = 0 for (const t of torrents) { if (!t || (t.category || '').toLowerCase() !== CONFIG.category) continue const addedTime = getAddedTime(t) if (!addedTime) missingAddedTime++ const stats = await getEffectiveStats(t) t.effectiveRatio = stats.effectiveRatio t.effectiveUpload = stats.effectiveUpload t.actualSize = (Number(t.size) || 0) * (Number(t.progress) || 0) // 实际磁盘占用 t.isInvalid = checkIsInvalid(t) // 站点失效标记 t.timeActive = Math.max(0, now - (addedTime || now)) t.recentUploadSpeed = await getRecentUploadSpeed(t, now) skyTorrents.push(t) } debugLog(`[${clientId}] sky 分类种子: ${skyTorrents.length},缺少添加时间: ${missingAddedTime}`) const gracefulDelete = async (torrent, reason, extraInfo = '') => { try { if (client.reannounceTorrent) { await client.reannounceTorrent(torrent) await new Promise(r => setTimeout(r, 2000)) } const extraText = extraInfo ? ` | ${extraInfo}` : '' logger.info(`[${SCRIPT_NAME}] 删除原因: ${reason} | 种子: ${torrent.name} | 有效Ratio: ${torrent.effectiveRatio.toFixed(3)} | 状态: ${torrent.state}${extraText}`) await client.deleteTorrent(torrent, { alias: '脚本自动' }) torrent.__skyDeleted = true return true } catch (e) { logger.error(`[${SCRIPT_NAME}] 删除失败 | 原因: ${reason} | 种子: ${torrent.name} | 错误: ${formatError(e)}`) return false } } // ================= 达标删除 ================= let freedSpace = 0 let ratioDeleted = 0 for (const torrent of skyTorrents) { if (torrent.effectiveRatio >= CONFIG.maxRatio) { const reason = `达标删除 / 有效Ratio ${torrent.effectiveRatio.toFixed(3)} >= ${CONFIG.maxRatio}` const extraInfo = `预计释放: ${formatSize(torrent.actualSize || 0)}` if (await gracefulDelete(torrent, reason, extraInfo)) { freedSpace += torrent.actualSize ratioDeleted++ } } } // ================= 空闲种主动清理 ================= let idleDeleted = 0 if (CONFIG.idleCleanup.enabled) { for (const torrent of skyTorrents) { if (torrent.__skyDeleted || torrent.effectiveRatio >= CONFIG.maxRatio) continue if (CONFIG.idleCleanup.maxDeletePerRun > 0 && idleDeleted >= CONFIG.idleCleanup.maxDeletePerRun) break const idleCheck = await checkIsIdleForCleanup(torrent, now) if (!idleCheck.matched) continue const reason = `空闲删除 / ${idleCheck.reason}` const extraInfo = `预计释放: ${formatSize(torrent.actualSize || 0)}` if (await gracefulDelete(torrent, reason, extraInfo)) { freedSpace += torrent.actualSize idleDeleted++ } } } // 计算达标删除、空闲删除后的虚拟空间 let virtualFreeSpace = currentFreeSpace + freedSpace const shouldCleanSpace = currentFreeSpace < CONFIG.minFreeSpace || virtualFreeSpace < CONFIG.minFreeSpace debugLog(`[${clientId}] 达标删除: ${ratioDeleted},空闲删除: ${idleDeleted},虚拟剩余空间: ${formatSize(virtualFreeSpace)},触发空间清理: ${shouldCleanSpace ? '是' : '否'}`) // ================= 空间清理 ================= if (shouldCleanSpace) { const candidateList = [] for (const t of skyTorrents) { if (t.__skyDeleted || t.effectiveRatio >= CONFIG.maxRatio) continue // 站点失效种子最高优先级,直接进入候选 if (t.isInvalid) { t.cleanupTag = getCleanupTag(t) candidateList.push({ torrent: t }) continue } const have = (t.progress > 0) const isUnderDelay = t.timeActive < CONFIG.resumeDelay const isUnderPostLimitGrace = !hasExitedProtection(t) // 正常清理保护初始低限速期、解除限速观察期、优质活跃未完成种 if (!isPanic) { if (isUnderPostLimitGrace) continue if (isHighValueActive(t)) continue } if (isPanic) { if (isUnderDelay && !have) continue } t.cleanupTag = getCleanupTag(t) candidateList.push({ torrent: t }) } debugLog(`[${clientId}] 空间清理候选: ${candidateList.length}`) // 排序逻辑 candidateList.sort((a, b) => { const tA = a.torrent const tB = b.torrent const tagDiff = CLEANUP_TAG_PRIORITY[tA.cleanupTag] - CLEANUP_TAG_PRIORITY[tB.cleanupTag] if (tagDiff !== 0) return tagDiff // 同标签内,当前上传速度慢的优先删除 const upDiff = (tA.upspeed || 0) - (tB.upspeed || 0) if (Math.abs(upDiff) > CONFIG.uploadSpeedPriorityDiff) { return upDiff } // 上传速度接近时,优先删除添加时间更早的种子 const addedTimeDiff = getAddedTime(tA) - getAddedTime(tB) if (Math.abs(addedTimeDiff) > CONFIG.addedTimePriorityDiff) { return addedTimeDiff } // 添加时间也接近时,优先删除有效Ratio更低的种子 const ratioDiff = (tA.effectiveRatio || 0) - (tB.effectiveRatio || 0) if (Math.abs(ratioDiff) > CONFIG.ratioPriorityDiff) { return ratioDiff } // Ratio也接近时,才用占用空间大的作为兜底排序 const sizeDiff = tB.actualSize - tA.actualSize if (sizeDiff !== 0) return sizeDiff // 普通报错是最后的轻微兜底因素 const aErr = (tA.state || '').toLowerCase().includes('error') const bErr = (tB.state || '').toLowerCase().includes('error') if (aErr !== bErr) return aErr ? -1 : 1 return 0 }) for (const candidate of candidateList) { if (virtualFreeSpace > CONFIG.targetFreeSpace) break const nextFreeSpace = virtualFreeSpace + candidate.torrent.actualSize const reason = buildCleanupReason(candidate.torrent, isPanic) const extraInfo = `预计空间: ${formatSize(virtualFreeSpace)} -> ${formatSize(nextFreeSpace)}` if (await gracefulDelete(candidate.torrent, reason, extraInfo)) { virtualFreeSpace = nextFreeSpace } } } // ================= 恢复与限速逻辑 ================= if (currentFreeSpace > CONFIG.panicSpace) { const pausedStates = ['pausedDL', 'pausedUP', 'Stopped', 'stopped'] const initialLimitByte = CONFIG.initialUploadLimit * 1024 const standardLimitByte = CONFIG.standardUploadLimit * 1024 const limitStats = { initialSet: 0, standardSet: 0, paused: 0, unchanged: 0, resumed: 0, failed: 0, skipped: 0 } const canSetSpeedLimit = typeof client.setSpeedLimit === 'function' const canPauseTorrent = typeof client.pauseTorrent === 'function' if (!canSetSpeedLimit) { logger.error(`[${SCRIPT_NAME}] [${clientId}] 下载器不支持 setSpeedLimit,限速逻辑无法执行`) } for (const torrent of skyTorrents) { if (torrent.__skyDeleted) { limitStats.skipped++ continue } const addedTime = getAddedTime(torrent) const torrentName = torrent.name || torrent.hash || '未知种子' if (!torrent.hash) { limitStats.skipped++ logger.error(`[${SCRIPT_NAME}] [${clientId}] 种子缺少 hash,跳过限速: ${torrentName}`) continue } if (!addedTime) { limitStats.skipped++ logger.error(`[${SCRIPT_NAME}] [${clientId}] 种子缺少添加时间,跳过限速: ${torrentName}`) continue } const isUnderDelay = (now - addedTime < CONFIG.resumeDelay) const currentUpLimit = getCurrentUpLimit(torrent) let targetLimit = null let targetLabel = '' let shouldPause = false if (isUnderDelay) { if (CONFIG.initialUploadLimit < 0) { shouldPause = true targetLabel = '初始暂停' } else { targetLimit = initialLimitByte targetLabel = CONFIG.initialUploadLimit === 0 ? '初始不限速' : '初始限速' } } else { targetLimit = standardLimitByte targetLabel = '标准限速' } if (shouldPause) { if (pausedStates.includes(torrent.state)) { limitStats.unchanged++ continue } if (!canPauseTorrent) { logger.error(`[${SCRIPT_NAME}] [${clientId}] 下载器不支持 pauseTorrent,无法暂停: ${torrentName}`) limitStats.failed++ continue } try { await client.pauseTorrent(torrent.hash) limitStats.paused++ debugLog(`[${clientId}] 初始期暂停种子成功: ${torrentName}`) } catch (e) { limitStats.failed++ logger.error(`[${SCRIPT_NAME}] [${clientId}] 初始期暂停种子失败: ${torrentName} | ${formatError(e)}`) } continue } if (targetLimit === null) { limitStats.skipped++ continue } if ((CONFIG.initialUploadLimit <= 0 || isUnderDelay) && pausedStates.includes(torrent.state)) { if (typeof client.resumeTorrent !== 'function') { logger.error(`[${SCRIPT_NAME}] [${clientId}] 下载器不支持 resumeTorrent,无法恢复: ${torrentName}`) limitStats.failed++ } else { try { await client.resumeTorrent(torrent.hash) limitStats.resumed++ debugLog(`[${clientId}] 恢复种子成功: ${torrentName}`) } catch (e) { limitStats.failed++ logger.error(`[${SCRIPT_NAME}] [${clientId}] 恢复种子失败: ${torrentName} | ${formatError(e)}`) } } } if (currentUpLimit === targetLimit) { limitStats.unchanged++ continue } if (!canSetSpeedLimit) { limitStats.failed++ continue } try { await client.setSpeedLimit(torrent.hash, 'upload', targetLimit) if (isUnderDelay) { limitStats.initialSet++ } else { limitStats.standardSet++ } debugLog(`[${clientId}] 设置${targetLabel}成功: ${torrentName} | ${formatSpeed(currentUpLimit)} -> ${formatSpeed(targetLimit)}`) } catch (e) { limitStats.failed++ logger.error(`[${SCRIPT_NAME}] [${clientId}] 设置${targetLabel}失败: ${torrentName} | ${formatSpeed(currentUpLimit)} -> ${formatSpeed(targetLimit)} | ${formatError(e)}`) } } debugLog(`[${clientId}] 限速汇总: 初始限速${limitStats.initialSet},标准限速${limitStats.standardSet},暂停${limitStats.paused},已符合${limitStats.unchanged},恢复${limitStats.resumed},跳过${limitStats.skipped},失败${limitStats.failed}`) } else { debugLog(`[${clientId}] 剩余空间 ${formatSize(currentFreeSpace)} <= 恐慌线 ${formatSize(CONFIG.panicSpace)},跳过恢复与限速`) } } catch (e) { logger.error(`[${SCRIPT_NAME}] [${clientId}] 处理异常: ${formatError(e)}`) } } debugLog('本轮运行完成') } catch (e) { logger.error(`[${SCRIPT_NAME}] 脚本运行异常: ${formatError(e)}`) } } ~~~ # 空控速删种脚本功能与删种逻辑 这是一个 Vertex 定时脚本,用来管理 Vertex 中所有运行中下载器客户端里的 `sky` 分类种子。可实现空的自动刷流管理,最好配合错误种子重启脚本试用。 - 前1小时限速,1小时后放开限速。 - 空间不足时,按照最近平均速度、添加时间、体积等排序清理种子。 - 清理长时间无上传的种子。 脚本会遍历: ```js global.runningClient ``` 因此它不只管理 Vertex 所在 VPS 的 qB,也会管理 Vertex 中配置并运行的其他远程 qB。 ## 配置分组 当前脚本把配置按功能拆成几组: ```js BASE_CONFIG RATIO_CONFIG SPACE_CLEANUP_CONFIG SPEED_LIMIT_CONFIG IDLE_CLEANUP_CONFIG ``` 底部再合并成统一的 `CONFIG`,方便脚本内部复用。 ## 当前配置摘要 基础配置: ```js category: 'sky' debug: true ``` 有效 Ratio: ```js resumeDelay: 58 * 60 maxRatio: 3.1 ``` 空间清理: ```js minFreeSpace: 25GB targetFreeSpace: 30GB panicSpace: 5GB protectUploadSpeed: 10MB/s postLimitGrace: 10 * 60 slowWindow: 10 * 60 slowUploadSpeed: 1MB/s stuckProgress: 0.995 stuckDownloadSpeed: 128KB/s uploadSpeedPriorityDiff: 10KB/s addedTimePriorityDiff: 30分钟 ratioPriorityDiff: 0.1 ``` 限速: ```js initialUploadLimit: 1024 standardUploadLimit: 460 * 1024 ``` 单位是 `KB/s`,调用下载器接口时会再乘以 `1024` 转成 bytes/s。 空闲清理: ```js enabled: true idleWindow: 30 * 60 minAge: 90 * 60 requireStandardLimit: true requireCurrentSpeedZero: true requireFlowRecord: true minEffectiveRatio: 0 maxDeletePerRun: 0 ``` `maxDeletePerRun: 0` 表示每轮不限制空闲删除数量。 ## 执行顺序 每个下载器内的执行顺序: ```text 1. 筛选 sky 分类种子 2. 预计算有效 Ratio、实际占用、tracker 状态、最近上传均速 3. 达标删除:有效 Ratio >= 3.1 4. 空闲种主动清理 5. 计算达标删除和空闲删除后的虚拟剩余空间 6. 空间不足时执行空间清理 7. 空间不低于恐慌线时维护恢复/限速 ``` 删除成功后会给种子打 `__skyDeleted` 标记,避免同一轮重复进入后续清理或限速逻辑。 ## 有效 Ratio 脚本不会直接使用 qB 的总 Ratio。 它会以: ```text 种子添加时间 + resumeDelay ``` 作为边界,从 Vertex 的 `torrent_flow` 表读取边界之后的上传记录,只计算初始低限速期之后的上传量。 当前 `resumeDelay` 为 `58分钟`。 计算方式: ```text 有效上传量 = 当前总上传量 - resumeDelay 边界时的上传量 有效 Ratio = 有效上传量 / 种子大小 ``` 有效 Ratio 达到 `3.1` 时,种子会被达标删除。 ## 空闲种主动清理 空闲清理默认开启,并且不要求空间不足。 它的目标是主动释放下载器空间,让 Vertex 更容易按空闲空间选择下载器,也让空间可以给其他站点使用。 空闲删除条件需要同时满足: ```text 属于 sky 分类 本轮未被删除 hash 存在 添加时间存在 添加时间 >= 90分钟 已经过初始低限速期和 10 分钟观察期 有效 Ratio >= minEffectiveRatio 当前处于标准限速 当前上传速度 = 0 当前下载速度 = 0 uploaded / downloaded 字段存在 最近 30 分钟有 torrent_flow 记录 最近 30 分钟上传增量 = 0 最近 30 分钟下载增量 = 0 ``` 默认 `requireFlowRecord: true`,所以最近 30 分钟没有流量记录时,不会直接当作空闲,避免 Vertex 统计缺失导致误删。 如果以后把 `requireFlowRecord` 改成 `false`,没有窗口记录且当前速度为 0 时,会被视作空闲。 ## 空间阈值 当前配置: ```js minFreeSpace: 25GB targetFreeSpace: 30GB panicSpace: 5GB ``` 含义: ```text 当前空间低于 25GB 时触发空间清理 达标删除、空闲删除后预计空间仍低于 25GB 时,也触发空间清理 一旦触发空间清理,会循环删除候选种子,直到预计剩余空间大于 30GB,或无候选可删 当前空间低于 5GB 时进入恐慌模式,部分保护会放宽 ``` ## 正常空间清理保护 正常空间清理时,以下种子不会进入候选: ```text 添加时间 < resumeDelay + postLimitGrace 优质活跃未完成种:未完成且当前上传速度 > 10MB/s ``` 当前配置下,正常空间清理会保护添加后约 `68分钟` 内的种子: ```text 58分钟初始低限速期 + 10分钟观察期 ``` 恐慌模式下,优质活跃种不再绝对保护,但会被打到 `优质活跃种兜底` 标签,排序靠后。 恐慌模式下,如果种子仍在初始低限速期且进度为 `0`,仍会保护;如果已有进度,则可以进入空间清理候选。 ## 候选标签 空间清理时,脚本会先给候选种子打标签。 标签优先级如下: ```text 1. 站点失效 2. 近完成低效种 3. 持续低上传种 4. 普通候选 5. 优质活跃种兜底 ``` ### 站点失效 tracker 信息包含以下情况时视为站点失效: ```text torrent banned Torrent not exists torrent not registered with this tracker unregistered torrent Invalid Torrent: ``` 脚本会检查: ```js trackerStatus tracker_status message error ``` ### 近完成低效种 满足: ```text 进度 >= 99.5% 进度 < 100% 已过初始低限速期和观察期 最近 10 分钟平均上传 < 1MB/s 当前下载速度 < 128KB/s ``` ### 持续低上传种 满足: ```text 已过初始低限速期和观察期 最近 10 分钟平均上传 < 1MB/s ``` ### 普通候选 不属于以上低效标签,但已经允许参与空间清理的普通种子。 ### 优质活跃种兜底 满足: ```text 未完成 当前上传速度 > 10MB/s ``` 正常空间清理时这类种子会被保护,不进入候选。恐慌模式下才会作为最后兜底。 ## 组内排序 同一标签内,再按以下阶梯排序: ```text 1. 当前上传速度差超过 10KB/s 时,当前上传更慢的优先删 2. 上传速度接近时,如果添加时间差超过 30分钟,更早添加的优先删 3. 添加时间也接近时,如果有效 Ratio 差超过 0.1,有效 Ratio 更低的优先删 4. 有效 Ratio 也接近时,占用空间更大的优先删 5. 前面都接近时,状态包含 error 的作为轻微兜底优先 ``` 也就是说,添加时间、有效 Ratio、占用空间不是和上传速度平级的独立优先级,而是在前一个指标接近时才继续比较。 ## 删种顺序总结 整体可以理解为: ```text 先删有效 Ratio 达标种 再主动清理空闲种 如果空间仍不足,再进入空间清理 空间清理先保护新种和优质活跃种 再优先清理站点失效种 再清理近完成但低效的卡死种 再清理持续低上传种 再处理普通候选 优质活跃种只在恐慌模式最后兜底 ``` 这套逻辑的目标是: ```text 保留真正有上传价值的种子 主动释放长期空闲种占用的空间 优先清理已经给过时间但仍然低效、占空间却收益很低的种子 ``` ## 限速逻辑 空间没有低于恐慌线时,脚本会维护上传限速: ```text 添加未满 58分钟:上传限速为 initialUploadLimit 添加满 58分钟:上传限速恢复为 standardUploadLimit ``` 当前配置: ```js initialUploadLimit: 1024 standardUploadLimit: 460 * 1024 ``` 含义: ```text 初始上传限速:1024KB/s 标准上传限速:460MB/s ``` `initialUploadLimit` 支持特殊值: ```text 0:初始期不限速 负数:初始期暂停 ``` 当前配置为正数,所以初始期会限速,不会暂停。 当剩余空间低于或等于 `panicSpace` 时,脚本会跳过恢复与限速逻辑,避免空间危急时额外恢复种子。 ## 日志 删除日志会记录: ```text 删除原因 种子名称 有效 Ratio 状态 当前上传 最近上传均速 当前下载 占用空间 预计释放空间 空间清理时的预计剩余空间变化 ``` 调试日志会记录: ```text 下载器数量 每个下载器的种子数量 sky 分类种子数量 缺少添加时间数量 达标删除数量 空闲删除数量 虚拟剩余空间 空间清理候选数量 限速汇总 ```
102天
相关话题回复浏览量活动