功能是1小时后开始下载或者前一小时限速一小时后不限速,分享率达标自动删除,空间不足时自动删种
根据大佬们的建议,已支持自动调整限速
initialUploadLimit设置为0为暂停模式,大于0是限速模式,具体值可以自己摸索,建议不要低于250
感谢各位大佬捧场
qb设置取消勾选为所有文件预分配磁盘空间

vt设置rss任务,勾选添加种子时暂停,分类设置为SKY


vt设置定时

cron:* * * * *
脚本:
async () => {
const moment = require('moment')
const util = require('../libs/util')
const clients = global.runningClient
const CONFIG = {
category: 'sky', // 分类
resumeDelay: 1 * 60 * 60, // 初始等待时长 (1小时)
maxRatio: 3.1, // 目标分享率 (扣除初始量)
minFreeSpace: 12 * 1024 * 1024 * 1024, // 触发清理的空间阈值
panicSpace: 5 * 1024 * 1024 * 1024, // 恐慌空间 (无视大部分保护)
gracePeriod: 10 * 60, // 暂停模式特有的保护时间
initialUploadLimit: 500, // 初始限速 (KB/s),0 为暂停
standardUploadLimit: 0 // 标准限速 (KB/s),0 为无限制
}
// 判定种子是否被站点删除/封禁
const checkIsInvalid = (torrent) => {
const { trackerStatus } = torrent
if (!trackerStatus) return false
const deletedMessages = [
"torrent banned",
"Torrent not exists",
"torrent not registered with this tracker",
"unregistered torrent",
"Invalid Torrent:"
]
const trackerMessage = trackerStatus.toLowerCase()
return deletedMessages.some(msg => trackerMessage.includes(msg.toLowerCase()))
}
// 获取扣除等待期上传量后的统计数据
const getEffectiveStats = async (torrent) => {
const boundaryTime = torrent.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 ? record.upload : 0
const effectiveUpload = Math.max(0, torrent.uploaded - uploadAtBoundary)
const effectiveRatio = torrent.size > 0 ? (effectiveUpload / torrent.size) : 0
return { effectiveUpload, effectiveRatio }
}
for (const clientId of Object.keys(clients)) {
const client = clients[clientId]
const maindata = client.maindata
const serverState = maindata?.serverState || maindata?.server_state
if (!maindata || !maindata.torrents || !serverState) continue
const torrents = maindata.torrents
const now = moment().unix()
const currentFreeSpace = serverState.free_space_on_disk || 0
const isPanic = currentFreeSpace < CONFIG.panicSpace
// 筛选并预计算
const skyTorrents = []
for (const t of torrents) {
if ((t.category || '').toLowerCase() === CONFIG.category) {
const stats = await getEffectiveStats(t)
t.effectiveRatio = stats.effectiveRatio
t.effectiveUpload = stats.effectiveUpload
t.actualSize = (t.size || 0) * (t.progress || 0) // 实际磁盘占用
t.isInvalid = checkIsInvalid(t) // 站点失效标记
skyTorrents.push(t)
}
}
const gracefulDelete = async (torrent, reason) => {
try {
if (client.reannounceTorrent) {
await client.reannounceTorrent(torrent)
await new Promise(r => setTimeout(r, 2000))
}
logger.info(`[脚本] ${reason}: ${torrent.name} | 有效Ratio: ${torrent.effectiveRatio.toFixed(3)} | 状态: ${torrent.state}`)
await client.deleteTorrent(torrent, { alias: '脚本自动' })
return true
} catch (e) {
logger.error(`[脚本] 删除失败: ${e.message}`)
return false
}
}
// ================= 达标删除 =================
let freedSpace = 0
for (const torrent of skyTorrents) {
if (torrent.effectiveRatio >= CONFIG.maxRatio) {
if (await gracefulDelete(torrent, ' 达标删除')) {
freedSpace += torrent.actualSize
}
}
}
// 计算达标删除后的虚拟空间
const virtualFreeSpace = currentFreeSpace + freedSpace
// ================= 空间清理 =================
if (virtualFreeSpace < CONFIG.minFreeSpace) {
const candidateList = []
for (const t of skyTorrents) {
if (t.effectiveRatio >= CONFIG.maxRatio) continue
const isError = t.state.toLowerCase().includes('error')
// 失效种子和常规报错优先进入候选
if (t.isInvalid || isError) {
candidateList.push({ torrent: t })
continue
}
const have = (t.progress > 0)
const timeActive = now - t.addedTime
const isUnderDelay = timeActive < CONFIG.resumeDelay
const isUnderGrace = (CONFIG.initialUploadLimit === 0)
? (timeActive < (CONFIG.resumeDelay + CONFIG.gracePeriod))
: false
if (isPanic) {
if (isUnderDelay && !have) continue
} else {
if (isUnderDelay) continue
if (isUnderGrace) continue
}
candidateList.push({ torrent: t })
}
// 排序逻辑
candidateList.sort((a, b) => {
const tA = a.torrent
const tB = b.torrent
// 站点已删种子绝对优先
if (tA.isInvalid !== tB.isInvalid) return tA.isInvalid ? -1 : 1
// 常规报错次优先
const aErr = tA.state.toLowerCase().includes('error')
const bErr = tB.state.toLowerCase().includes('error')
if (aErr !== bErr) return aErr ? -1 : 1
// 恐慌模式先删除占用大的
if (isPanic) {
return tB.actualSize - tA.actualSize
}
// 正常模式:先看上传速度差异
const upDiff = tA.upspeed - tB.upspeed
if (Math.abs(upDiff) > 10 * 1024) {
return upDiff
}
// 速度差不多(<=10KB/s)时,删除占用大的
const sizeDiff = tB.actualSize - tA.actualSize
if (sizeDiff !== 0) return sizeDiff
// 保护下载中的种子
if (tA.progress === 1 && tB.progress < 1) return -1
if (tB.progress === 1 && tA.progress < 1) return 1
return 0
})
if (candidateList.length > 0) {
const mode = isPanic ? " [恐慌清理]" : " [空间清理]"
await gracefulDelete(candidateList[0].torrent, mode)
}
}
// ================= 恢复与限速逻辑 =================
if (currentFreeSpace > CONFIG.panicSpace) {
const pausedStates = ['pausedDL', 'pausedUP', 'Stopped', 'stopped']
for (const torrent of skyTorrents) {
const isUnderDelay = (now - torrent.addedTime < CONFIG.resumeDelay)
const currentUpLimit = torrent.originProp ? torrent.originProp.up_limit : 0
const initialLimitByte = CONFIG.initialUploadLimit * 1024
const standardLimitByte = CONFIG.standardUploadLimit * 1024
if (CONFIG.initialUploadLimit > 0) {
if (isUnderDelay) {
if (pausedStates.includes(torrent.state)) {
await client.resumeTorrent(torrent.hash)
}
if (currentUpLimit !== initialLimitByte) {
await client.setSpeedLimit(torrent.hash, 'upload', initialLimitByte)
}
} else {
if (currentUpLimit !== standardLimitByte) {
await client.setSpeedLimit(torrent.hash, 'upload', standardLimitByte)
}
}
} else {
if (!isUnderDelay) {
if (currentUpLimit !== standardLimitByte) {
await client.setSpeedLimit(torrent.hash, 'upload', standardLimitByte)
}
if (pausedStates.includes(torrent.state)) {
await client.resumeTorrent(torrent.hash)
}
}
}
}
}
}
}
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)}`)
}
}