蓝影论坛 Logo
27浏览量
1参与者
活动 98天
✨这是 parkertaylor 首次发帖 - 让我们欢迎他/她加入社区吧!
parkertaylor
楼主
~~~ async () => { const moment = require('moment') const util = require('../libs/util') const clients = global.runningClient || {} const BASE_CONFIG = { scriptName: '普通站点空间空闲清理', debug: true } const SCOPE_CONFIG = { includeClientAliases: [], // 空数组表示处理所有下载器;非空时只处理这些下载器别名 excludeClientAliases: [], // 排除这些下载器别名,优先级高于 includeClientAliases includeCategories: [], // 空数组表示处理所有分类;非空时只处理这些分类 excludeCategories: ['KEEP', 'SKY', '长期'] // 排除这些分类,优先级高于 includeCategories } const IDLE_CLEANUP_CONFIG = { enabled: true, // 默认开启空闲种清理 idleWindow: 30 * 60, // 最近 30 分钟平均速度低于阈值才算空闲 unfinishedMinAge: 1 * 60 * 60, // 未完成种添加超过该时间后也允许空闲清理 idleUploadSpeedThreshold: 0, // 空闲窗口平均上传速度阈值 (bytes/s) idleDownloadSpeedThreshold: 0, // 空闲窗口平均下载速度阈值 (bytes/s) requireFlowRecord: true, // 最近窗口必须有流量记录;没有记录不当作空闲 maxDeletePerRun: 0 // 每轮最多删多少空闲种;0 表示不限制 } const SPACE_CLEANUP_CONFIG = { enabled: true, minFreeSpace: 20 * 1024 * 1024 * 1024, // 低于该空间触发空间清理 targetFreeSpace: 20 * 1024 * 1024 * 1024, // 清理到该空间后停止 panicSpace: 5 * 1024 * 1024 * 1024, // 仅用于日志标记,当前策略不额外保护 unfinishedMinAge: 1 * 60 * 60, // 未完成种添加超过该时间后可参与空间清理 recentUploadWindow: 3 * 60, // 最近 3 分钟上传均速用于排序 slowUploadSpeed: 1 * 1024 * 1024, // 低上传标签阈值,仅影响日志/标签 recentUploadSpeedPriorityDiff: 64 * 1024, // 上传均速差超过 64KB/s 才认为有明显差异 sizePriorityDiff: 1 * 1024 * 1024 * 1024, // 占用空间差超过 1GB 才按空间大小排序 downloadSpeedPriorityDiff: 128 * 1024, // 当前下载速度差超过 128KB/s 才按下载速度排序 addedTimePriorityDiff: 30 * 60 // 添加时间差超过 30 分钟才按更早添加排序 } const CONFIG = { ...BASE_CONFIG, scope: SCOPE_CONFIG, idleCleanup: IDLE_CLEANUP_CONFIG, spaceCleanup: SPACE_CLEANUP_CONFIG } const formatError = (error) => { if (!error) return '未知错误' return error.stack || error.message || String(error) } const debugLog = (message) => { if (CONFIG.debug) { logger.info(`[${CONFIG.scriptName}][DEBUG] ${message}`) } } // Vertex 不同版本/下载器可能把 torrents 放成数组或对象,这里统一转成数组。 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 isFinished = (torrent) => { return (Number(torrent.progress) || 0) >= 1 } const normalizeAlias = (value) => { return String(value || '').trim() } const normalizeCategory = (value) => { return String(value || '').trim().toLowerCase() } const normalizeAliasList = (list) => { return Array.isArray(list) ? list.map(normalizeAlias).filter(Boolean) : [] } const normalizeCategoryList = (list) => { return Array.isArray(list) ? list.map(normalizeCategory) : [] } const getClientAlias = (client, clientId) => { return normalizeAlias(client?.alias || client?._client?.alias || clientId) } const isClientInScope = (client, clientId) => { const scope = CONFIG.scope const alias = getClientAlias(client, clientId) const included = normalizeAliasList(scope.includeClientAliases) const excluded = normalizeAliasList(scope.excludeClientAliases) if (excluded.includes(alias)) return false if (included.length > 0 && !included.includes(alias)) return false return true } const getCategory = (torrent) => { return normalizeCategory(torrent.category) } const isCategoryInScope = (torrent) => { const scope = CONFIG.scope const category = getCategory(torrent) const included = normalizeCategoryList(scope.includeCategories) const excluded = normalizeCategoryList(scope.excludeCategories) if (excluded.includes(category)) return false if (included.length > 0 && !included.includes(category)) return false return true } // tracker 明确提示种子已删除/未注册时,空间清理中最高优先级删除。 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 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` // 用 torrent_flow 计算最近几分钟上传均速,比 qB 瞬时 upspeed 更适合做删种排序。 const getRecentUploadSpeed = async (torrent, now) => { if (!torrent.hash) return Number(torrent.upspeed) || 0 const windowStart = now - CONFIG.spaceCleanup.recentUploadWindow 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 Number(torrent.upspeed) || 0 } const elapsed = Math.max(1, now - Number(record.time)) const uploadDiff = Math.max(0, (Number(torrent.uploaded) || 0) - (Number(record.upload) || 0)) return uploadDiff / elapsed } // 空闲清理看 idleWindow 内上传/下载平均速度;缺记录时默认不当作空闲,避免统计缺失误删。 const getIdleWindowStats = async (torrent, now) => { const idleConfig = CONFIG.idleCleanup if (!torrent.hash) { return { hasRecord: false, uploadDiff: 0, downloadDiff: 0, avgUploadSpeed: 0, avgDownloadSpeed: 0, reason: '缺少 hash' } } if (torrent.uploaded === undefined || torrent.downloaded === undefined) { return { hasRecord: false, uploadDiff: 0, downloadDiff: 0, avgUploadSpeed: 0, avgDownloadSpeed: 0, 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 { hasRecord: false, uploadDiff: 0, downloadDiff: 0, avgUploadSpeed: 0, avgDownloadSpeed: 0, reason: '空闲窗口缺少流量记录' } } if (record.upload === undefined || record.download === undefined) { return { hasRecord: false, uploadDiff: 0, downloadDiff: 0, avgUploadSpeed: 0, avgDownloadSpeed: 0, 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)) const elapsed = Math.max(1, now - Number(record.time)) return { hasRecord: true, uploadDiff, downloadDiff, avgUploadSpeed: uploadDiff / elapsed, avgDownloadSpeed: downloadDiff / elapsed, reason: '' } } const isOldEnoughForCleanup = (torrent, seconds) => { return isFinished(torrent) || (Number(torrent.timeActive) || 0) >= seconds } // 空闲删除必须同时满足窗口平均速度低于阈值,以及未完成种达到最小年龄。 const checkIsIdleForCleanup = (torrent) => { const idleConfig = CONFIG.idleCleanup if (!idleConfig.enabled) return { matched: false, reason: '空闲清理未开启' } if (torrent.__generalDeleted) return { matched: false, reason: '本轮已删除' } if (!isCategoryInScope(torrent)) return { matched: false, reason: '分类不在处理范围' } if (!isOldEnoughForCleanup(torrent, idleConfig.unfinishedMinAge)) { return { matched: false, reason: '未完成种未达到清理年龄' } } if (!torrent.idleStats.hasRecord) { if (idleConfig.requireFlowRecord) { return { matched: false, reason: torrent.idleStats.reason } } const fallbackUploadSpeed = Number(torrent.upspeed) || 0 const fallbackDownloadSpeed = Number(torrent.dlspeed) || 0 const fallbackMatched = fallbackUploadSpeed <= idleConfig.idleUploadSpeedThreshold && fallbackDownloadSpeed <= idleConfig.idleDownloadSpeedThreshold return fallbackMatched ? { matched: true, reason: `空闲窗口无流量记录,当前速度低于阈值 上传${formatSpeed(fallbackUploadSpeed)} 下载${formatSpeed(fallbackDownloadSpeed)}` } : { matched: false, reason: `空闲窗口无流量记录,当前速度高于阈值 上传${formatSpeed(fallbackUploadSpeed)} 下载${formatSpeed(fallbackDownloadSpeed)}` } } if (torrent.idleStats.avgUploadSpeed > idleConfig.idleUploadSpeedThreshold || torrent.idleStats.avgDownloadSpeed > idleConfig.idleDownloadSpeedThreshold) { return { matched: false, reason: `窗口平均速度高于阈值 上传${formatSpeed(torrent.idleStats.avgUploadSpeed)} 下载${formatSpeed(torrent.idleStats.avgDownloadSpeed)}` } } return { matched: true, reason: `持续低速${Math.floor(idleConfig.idleWindow / 60)}分钟 上传${formatSpeed(torrent.idleStats.avgUploadSpeed)} 下载${formatSpeed(torrent.idleStats.avgDownloadSpeed)}` } } const getCleanupTag = (torrent) => { if (torrent.isInvalid) return '站点失效' if (torrent.idleMatched) return '空闲种' if (!isFinished(torrent) && (torrent.recentUploadSpeed || 0) < CONFIG.spaceCleanup.slowUploadSpeed) { return '未完成低上传' } if (isFinished(torrent) && (torrent.recentUploadSpeed || 0) < CONFIG.spaceCleanup.slowUploadSpeed) { return '已完成低上传' } return '普通候选' } const buildReason = (prefix, torrent) => { const addedTime = getAddedTime(torrent) const ageMinutes = addedTime ? Math.floor((moment().unix() - addedTime) / 60) : 0 const parts = [ prefix, `分类${torrent.category || '无'}`, getCleanupTag(torrent), `进度${((torrent.progress || 0) * 100).toFixed(1)}%`, `添加${ageMinutes}分钟前`, `当前上传${formatSpeed(torrent.upspeed || 0)}`, `${Math.floor(CONFIG.spaceCleanup.recentUploadWindow / 60)}分钟上传均速${formatSpeed(torrent.recentUploadSpeed || 0)}`, `当前下载${formatSpeed(torrent.dlspeed || 0)}`, `${Math.floor(CONFIG.idleCleanup.idleWindow / 60)}分钟空闲上传均速${formatSpeed(torrent.idleStats.avgUploadSpeed || 0)}`, `${Math.floor(CONFIG.idleCleanup.idleWindow / 60)}分钟空闲下载均速${formatSpeed(torrent.idleStats.avgDownloadSpeed || 0)}`, `30分钟上传增量${formatSize(torrent.idleStats.uploadDiff || 0)}`, `30分钟下载增量${formatSize(torrent.idleStats.downloadDiff || 0)}`, `占用${formatSize(torrent.actualSize || 0)}` ] if (torrent.isInvalid) parts.push('tracker失效') return parts.join(' / ') } // 空间清理排序使用“差距阈值”:指标差距很小时进入下一层,避免被微小抖动带偏。 const sortCleanupCandidates = (a, b) => { const tA = a.torrent const tB = b.torrent if (tA.isInvalid !== tB.isInvalid) return tA.isInvalid ? -1 : 1 if (tA.idleMatched !== tB.idleMatched) return tA.idleMatched ? -1 : 1 // 先看最近上传均速;只有差距明显时才按均速删除。 const recentUpDiff = (tA.recentUploadSpeed || 0) - (tB.recentUploadSpeed || 0) if (Math.abs(recentUpDiff) > CONFIG.spaceCleanup.recentUploadSpeedPriorityDiff) { return recentUpDiff } // 上传收益接近时,优先释放空间更大的种子。 const sizeDiff = (tB.actualSize || 0) - (tA.actualSize || 0) if (Math.abs(sizeDiff) > CONFIG.spaceCleanup.sizePriorityDiff) { return sizeDiff } // 空间收益也接近时,再看当前下载速度,下载更慢的先删。 const downDiff = (tA.dlspeed || 0) - (tB.dlspeed || 0) if (Math.abs(downDiff) > CONFIG.spaceCleanup.downloadSpeedPriorityDiff) { return downDiff } // 前面都接近时,优先清理添加更久的种子。 const addedTimeDiff = getAddedTime(tA) - getAddedTime(tB) if (Math.abs(addedTimeDiff) > CONFIG.spaceCleanup.addedTimePriorityDiff) { return addedTimeDiff } return 0 } try { const clientIds = Object.keys(clients) debugLog(`开始运行,下载器数量: ${clientIds.length}`) if (clientIds.length === 0) { logger.error(`[${CONFIG.scriptName}] 未找到正在运行的下载器`) return } for (const clientId of clientIds) { try { const client = clients[clientId] const clientAlias = getClientAlias(client, clientId) const clientLabel = `${clientAlias}/${clientId}` if (!isClientInScope(client, clientId)) { debugLog(`[${clientLabel}] 下载器别名不在处理范围,跳过`) continue } const maindata = client && client.maindata const serverState = maindata && (maindata.serverState || maindata.server_state) if (!client || !maindata || !maindata.torrents || !serverState) { logger.error(`[${CONFIG.scriptName}] [${clientLabel}] 下载器信息不完整,跳过`) 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.spaceCleanup.panicSpace debugLog(`[${clientLabel}] 扫描 ${torrents.length} 个种子,剩余空间 ${formatSize(currentFreeSpace)},恐慌空间 ${isPanic ? '是' : '否'}`) // 预计算后续排序/清理要用的派生字段,避免排序时反复查数据库。 const managedTorrents = [] let outOfScopeCategoryCount = 0 let missingAddedTime = 0 for (const torrent of torrents) { if (!torrent) continue if (!isCategoryInScope(torrent)) { outOfScopeCategoryCount++ continue } const addedTime = getAddedTime(torrent) if (!addedTime) missingAddedTime++ torrent.timeActive = Math.max(0, now - (addedTime || now)) torrent.actualSize = (Number(torrent.size ?? torrent.total_size) || 0) * (Number(torrent.progress) || 0) torrent.isInvalid = checkIsInvalid(torrent) torrent.recentUploadSpeed = await getRecentUploadSpeed(torrent, now) torrent.idleStats = await getIdleWindowStats(torrent, now) torrent.idleMatched = checkIsIdleForCleanup(torrent).matched managedTorrents.push(torrent) } debugLog(`[${clientLabel}] 可管理种子 ${managedTorrents.length} 个,分类范围外跳过 ${outOfScopeCategoryCount} 个,缺少添加时间 ${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(`[${CONFIG.scriptName}] [${clientLabel}] 删除原因: ${reason} | 种子: ${torrent.name} | 状态: ${torrent.state}${extraText}`) await client.deleteTorrent(torrent, { alias: '脚本自动' }) torrent.__generalDeleted = true return true } catch (e) { logger.error(`[${CONFIG.scriptName}] [${clientLabel}] 删除失败 | 种子: ${torrent.name} | 原因: ${reason} | 错误: ${formatError(e)}`) return false } } let freedSpace = 0 let idleDeleted = 0 // 先做主动空闲清理,让 Vertex 和其他站点能更早拿到空闲空间。 if (CONFIG.idleCleanup.enabled) { for (const torrent of managedTorrents) { if (torrent.__generalDeleted) continue if (CONFIG.idleCleanup.maxDeletePerRun > 0 && idleDeleted >= CONFIG.idleCleanup.maxDeletePerRun) break const idleCheck = checkIsIdleForCleanup(torrent) if (!idleCheck.matched) continue const reason = buildReason(`空闲删除 / ${idleCheck.reason}`, torrent) const extraInfo = `预计释放: ${formatSize(torrent.actualSize || 0)}` if (await gracefulDelete(torrent, reason, extraInfo)) { freedSpace += torrent.actualSize || 0 idleDeleted++ } } } let virtualFreeSpace = currentFreeSpace + freedSpace let spaceDeleted = 0 const shouldCleanSpace = CONFIG.spaceCleanup.enabled && (currentFreeSpace < CONFIG.spaceCleanup.minFreeSpace || virtualFreeSpace < CONFIG.spaceCleanup.minFreeSpace) debugLog(`[${clientLabel}] 空闲删除 ${idleDeleted} 个,虚拟剩余空间 ${formatSize(virtualFreeSpace)},触发空间清理 ${shouldCleanSpace ? '是' : '否'}`) if (shouldCleanSpace) { const candidateList = [] // 空间清理不看最低 ratio/做种时间;只跳过范围外分类和未达到最小年龄的未完成种。 for (const torrent of managedTorrents) { if (torrent.__generalDeleted) continue if (!torrent.isInvalid && !isOldEnoughForCleanup(torrent, CONFIG.spaceCleanup.unfinishedMinAge)) continue torrent.idleMatched = torrent.idleMatched || checkIsIdleForCleanup(torrent).matched candidateList.push({ torrent }) } candidateList.sort(sortCleanupCandidates) debugLog(`[${clientLabel}] 空间清理候选 ${candidateList.length} 个`) for (const candidate of candidateList) { if (virtualFreeSpace > CONFIG.spaceCleanup.targetFreeSpace) break const torrent = candidate.torrent const nextFreeSpace = virtualFreeSpace + (torrent.actualSize || 0) const reason = buildReason('空间清理', torrent) const extraInfo = `预计空间: ${formatSize(virtualFreeSpace)} -> ${formatSize(nextFreeSpace)}` if (await gracefulDelete(torrent, reason, extraInfo)) { virtualFreeSpace = nextFreeSpace spaceDeleted++ } } } debugLog(`[${clientLabel}] 本轮完成:空闲删除 ${idleDeleted},空间清理删除 ${spaceDeleted},预计剩余空间 ${formatSize(virtualFreeSpace)}`) } catch (e) { logger.error(`[${CONFIG.scriptName}] [${clientId}] 处理异常: ${formatError(e)}`) } } debugLog('全部下载器处理完成') } catch (e) { logger.error(`[${CONFIG.scriptName}] 脚本运行异常: ${formatError(e)}`) } } ~~~ # 普通站点空间 / 空闲清理脚本说明 脚本文件: ```text 普通站点空间空闲清理-定时脚本.js ``` ## 更新日志 ### v1.2 ```text 新增下载器别名范围配置,可默认处理全部下载器,也可按别名包含/排除指定下载器。 新增分类范围配置,可默认处理全部分类,也可按分类包含/排除指定分类;默认继续排除 KEEP/SKY/长期。 空闲清理改为使用 idleWindow 内平均上传/下载速度阈值,默认阈值为 0,保持原有无流量行为。 空间清理排序继续使用 recentUploadWindow 内近期平均上传速度,避免使用瞬时上传速度。 删除日志新增空闲窗口平均上传/下载速度,便于调参和排查误删。 ``` 这是一个 Vertex 定时脚本,用来清理普通站点中的慢速种、空闲种,核心目标是释放下载器空间,让 Vertex 可以按空闲空间调度下载器,也让空间可以被其他站点使用。 ## 适用范围 脚本会遍历: ```js global.runningClient ``` 默认情况下,Vertex 中所有正在运行的下载器都会被处理。 脚本不按站点白名单处理,而是按下载器别名和种子分类控制作用范围: ```js const SCOPE_CONFIG = { includeClientAliases: [], excludeClientAliases: [], includeCategories: [], excludeCategories: ['KEEP', 'SKY', '长期'] } ``` 含义: ```text includeClientAliases 为空:处理所有下载器 includeClientAliases 非空:只处理这些下载器别名 excludeClientAliases:排除这些下载器别名,优先级高于 includeClientAliases includeCategories 为空:处理所有分类 includeCategories 非空:只处理这些分类 excludeCategories:排除这些分类,优先级高于 includeCategories ``` 下载器别名使用 Vertex 下载器列表中的“别名”字段,精确匹配;分类匹配会忽略大小写和前后空格。无分类种子可以用空字符串 `''` 匹配。 示例: ```js // 只处理两个指定下载器,分类仍按默认排除 KEEP/SKY/长期。 includeClientAliases: ['01NC', 'hz01-640g-kong'] excludeClientAliases: [] includeCategories: [] excludeCategories: ['KEEP', 'SKY', '长期'] ``` ```js // 处理所有下载器,但只处理 movie/rss 分类。 includeClientAliases: [] excludeClientAliases: [] includeCategories: ['movie', 'rss'] excludeCategories: [] ``` ## 为什么用定时脚本 空间清理和空闲清理不建议用 Vertex 删种规则做。 原因是这套逻辑需要: - 查询最近 3 分钟上传均速。 - 查询最近 30 分钟上传 / 下载均速和增量。 - 按近期上传均速、占用空间、下载速度、添加时间排序。 - 先空闲清理,再按虚拟剩余空间决定是否继续空间清理。 - 多下载器统一处理,并避免同一轮重复删同一个种子。 这些属于动态调度逻辑,定时脚本比删种规则更合适。 ## 执行顺序 每轮执行顺序: ```text 1. 遍历所有 runningClient 2. 按下载器别名筛选下载器 3. 按分类范围筛选种子 4. 预计算每个种子的状态 5. 先执行空闲种主动清理 6. 计算虚拟剩余空间 7. 如果空间低于阈值,再执行空间清理 8. 写入详细日志 ``` ## 空闲清理 默认开启: ```js enabled: true ``` 默认清理全部匹配的空闲种: ```js maxDeletePerRun: 0 ``` `0` 表示不限制每轮删除数量。 空闲窗口默认 30 分钟: ```js idleWindow: 30 * 60 ``` 一个种子会被空闲清理,需要同时满足: ```text 所在下载器别名在处理范围内 种子分类在处理范围内 最近 30 分钟平均上传 <= idleUploadSpeedThreshold 最近 30 分钟平均下载 <= idleDownloadSpeedThreshold 已完成,或未完成但添加时间 >= unfinishedMinAge 最近 30 分钟有 torrent_flow 记录 ``` 未完成种默认添加超过 1 小时后也允许被清理: ```js unfinishedMinAge: 1 * 60 * 60 ``` 这里的逻辑是:未完成种如果长时间没有上传/下载流量,就说明它对当前刷流和空间利用价值都很低。 当前 `idleUploadSpeedThreshold` 和 `idleDownloadSpeedThreshold` 都是 `0`,所以默认仍等价于最近 30 分钟没有上传/下载增量。以后可以按需改成例如 `64 * 1024`,表示最近 30 分钟平均速度不超过 `64KB/s` 就视为空闲。 ## 空间清理 空间清理默认开启: ```js enabled: true ``` 默认阈值: ```js minFreeSpace: 20GB targetFreeSpace: 20GB panicSpace: 5GB ``` 当当前剩余空间或空闲清理后的虚拟剩余空间低于 `minFreeSpace` 时,触发空间清理。 触发后会持续删除候选种子,直到虚拟剩余空间高于 `targetFreeSpace`,或没有候选种可删。 ## 空间清理候选 候选条件: ```text 所在下载器别名在处理范围内 种子分类在处理范围内 站点失效:直接作为候选 已完成:可作为候选 未完成:添加时间 >= unfinishedMinAge 后可作为候选 ``` 脚本不检查最低 Ratio,也不检查最少做种时间。 原因是这套脚本的目标不是保种达标,而是清理慢速和低空间收益种子。 ## 空间清理排序 排序优先级: ```text 1. 站点失效优先 2. 空闲种优先 3. 最近 3 分钟上传均速差距明显时,均速越低越先删 4. 上传均速相近时,实际占用空间差距明显的,大空间优先删 5. 占用空间也相近时,当前下载速度差距明显的,下载更慢优先删 6. 仍然相近时,添加时间差距明显的,更早添加优先删 ``` 最近上传均速窗口默认 3 分钟: ```js recentUploadWindow: 3 * 60 ``` 相近阈值默认: ```js recentUploadSpeedPriorityDiff: 64 * 1024 // 64KB/s sizePriorityDiff: 1 * 1024 * 1024 * 1024 // 1GB downloadSpeedPriorityDiff: 128 * 1024 // 128KB/s addedTimePriorityDiff: 30 * 60 // 30分钟 ``` 空间清理不再使用当前上传速度排序,因为 `upspeed` 是瞬时值,容易被采样抖动影响。当前上传速度仍会记录在日志里,但空闲清理也改为使用窗口平均速度阈值判断。 注意:未完成且长时间低速,指的是上传低速。下载速度只作为排序参考,不作为保护理由。 ## 重要配置 作用范围: ```js const SCOPE_CONFIG = { includeClientAliases: [], // 空数组表示处理所有下载器;非空时只处理这些下载器别名 excludeClientAliases: [], // 排除这些下载器别名,优先级高于 includeClientAliases includeCategories: [], // 空数组表示处理所有分类;非空时只处理这些分类 excludeCategories: ['KEEP', 'SKY', '长期'] // 排除这些分类,优先级高于 includeCategories } ``` 空闲清理: ```js const IDLE_CLEANUP_CONFIG = { enabled: true, idleWindow: 30 * 60, unfinishedMinAge: 1 * 60 * 60, idleUploadSpeedThreshold: 0, idleDownloadSpeedThreshold: 0, requireFlowRecord: true, maxDeletePerRun: 0 } ``` 空间清理: ```js const SPACE_CLEANUP_CONFIG = { enabled: true, minFreeSpace: 20 * 1024 * 1024 * 1024, targetFreeSpace: 20 * 1024 * 1024 * 1024, panicSpace: 5 * 1024 * 1024 * 1024, unfinishedMinAge: 1 * 60 * 60, recentUploadWindow: 3 * 60, slowUploadSpeed: 1 * 1024 * 1024, recentUploadSpeedPriorityDiff: 64 * 1024, sizePriorityDiff: 1 * 1024 * 1024 * 1024, downloadSpeedPriorityDiff: 128 * 1024, addedTimePriorityDiff: 30 * 60 } ``` ## 使用建议 上线前建议先确认: ```text 1. includeClientAliases / excludeClientAliases 是否符合要处理的下载器范围。 2. includeCategories / excludeCategories 是否符合要处理的分类范围。 3. minFreeSpace / targetFreeSpace 是否符合盒子容量。 4. unfinishedMinAge 是否符合站点和刷流节奏。 5. Vertex 的 torrent_flow 表是否正常记录 upload / download。 ``` 如果想先保守观察,可以临时设置: ```js maxDeletePerRun: 3 ``` 确认日志符合预期后,再改回: ```js maxDeletePerRun: 0 ``` ## 日志 删除日志会包含: ```text 删除类型 分类 进度 添加时间 当前上传速度 最近 3 分钟上传均速 当前下载速度 30 分钟空闲上传均速 30 分钟空闲下载均速 30 分钟上传增量 30 分钟下载增量 占用空间 预计释放或预计空间变化 ``` 这些信息用于判断是否存在误删风险,以及后续调参。
98天
相关话题回复浏览量活动