PT 刷流 Vertex 刷流规则分享

MarkXiu 2026-09-03 22:03 1

Vertex 刷流规则脚本合集


本文整理自自己实际 Vertex 配置,纯粹是抛砖引玉,大佬轻喷。


这些脚本用于 qBittorrent 的自动删种、RSS 筛选和上传限速,分享出来供参考。不同 VPS 的 CPU、内存、磁盘容量、网络带宽、下载器版本和刷流目标不同,不建议直接照搬参数。


复制前,建议把自己的 VPS 配置和刷流目标交给 AI,让 AI 检查连接数、并发下载数、磁盘空间阈值、上传速度和删种条件。


重要资源建议设置为 keepKEEP 分类,并先使用测试任务验证。


一、定时脚本


定时脚本按固定周期执行,适合做上传限速、标签处理等周期性任务。


黑盒-三倍限速(主要防止上传太猛, 下载完成已经超过3倍分享率,浪费流量)



  • 执行周期:每分钟一次


功能:分享率达到 3 倍后把单种上传限速设为 1 MiB/s;低于 2.5 倍时恢复到 60 MiB/s;分类包含“发种”的任务跳过。


完整源码:


async () => {
/**
* Vertex 定时脚本:
* 对指定下载器里的种子按分享率自动切换单种上传限速。
*
* 规则:
* 1. 只检查配置中的下载器
* 2. 标签包含“发种”的种子跳过
* 3. 当 uploaded / size >= 3 时,设置单种上传限速为 1 MiB/s
* 4. 当 uploaded / size < 2.5 时,设置单种上传限速为 60 MiB/s
* 5. 只处理 size > 0、uploaded > 0、downloaded > 0 的种子
*/


const config = {
/**
* 达到这个分享率时,切到低速上传。
*/

enableLimitRatio: 3,

/**
* 低于这个分享率时,切到高速上传。
*/

disableLimitRatio: 2.5,

/**
* 低速限速,单位 Byte/s。
* 这里是 1 MiB/s。
*/

lowUploadLimit: 1 * 1024 * 1024,

/**
* 高速限速,单位 Byte/s。
* 这里是 60 MiB/s。
*/

highUploadLimit: 60 * 1024 * 1024,

/**
* 日志里展示用的文案。
*/

lowUploadLimitText: '1 MiB/s',
highUploadLimitText: '60 MiB/s',

/**
* 跳过带有这些标签的种子。
*/

skipTags: ['发种'],

/**
* 需要巡检的下载器列表。
*/

clients: [
{
name: 'hostdzire',
clientId:'YOUR_CLIENT_ID',
url: 'https://your-qbittorrent.example',
pass:'YOUR_PASSWORD'
},
{
name: 'takehost',
clientId:'YOUR_CLIENT_ID',
url: 'https://your-qbittorrent.example',
pass:'YOUR_PASSWORD'
}
]
};

const axios = require('axios');

logger.info('=========================================');
logger.info(`[分享率限速] 脚本启动,本次共检查 ${config.clients.length} 个下载器`);

for (const qb of config.clients) {
try {
const client = global.runningClient[qb.clientId];

if (!client || !client.maindata || !Array.isArray(client.maindata.torrents)) {
logger.info(`[${qb.name}] 缓存未就绪或下载器不可用,跳过`);
continue;
}

const torrents = client.maindata.torrents;
const toLowLimit = [];
const toHighLimit = [];
let skippedByTagCount = 0;

for (const t of torrents) {
const tags = String(t.tags || '');
const size = Number(t.size || 0);
const uploaded = Number(t.uploaded || 0);
const downloaded = Number(t.downloaded || 0);
const hash = String(t.hash || '').trim();

const shouldSkipByTag = config.skipTags.some(tag => tags.indexOf(tag) !== -1);
if (shouldSkipByTag) {
skippedByTagCount++;
continue;
}

if (!hash || size <= 0 || uploaded <= 0 || downloaded <= 0) {
continue;
}

const ratio = uploaded / size;
const currentLimit = Number(
t.upLimit !== undefined ? t.upLimit : (t.up_limit !== undefined ? t.up_limit : 0)
);

if (ratio >= config.enableLimitRatio && currentLimit !== config.lowUploadLimit) {
toLowLimit.push(t);
continue;
}

if (ratio < config.disableLimitRatio && currentLimit !== config.highUploadLimit) {
toHighLimit.push(t);
}
}

if (toLowLimit.length === 0 && toHighLimit.length === 0) {
logger.info(
`[${qb.name}] 巡检完成,无需处理;标签豁免数量: ${skippedByTagCount}`
);
continue;
}

logger.info(
`[${qb.name}] 准备处理:切低速 ${toLowLimit.length} 个,切高速 ${toHighLimit.length} 个;标签豁免数量: ${skippedByTagCount}`
);

const loginData = new URLSearchParams();
loginData.append('username', qb.user);
loginData.append('password', qb.pass);

const loginRes = await axios.post(
`${qb.url}/api/v2/auth/login`,
loginData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

const cookie = loginRes.headers['set-cookie'];
if (!cookie) {
logger.info(`[${qb.name}] 登录失败,未获取到 Cookie`);
continue;
}

const requestHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
Cookie: Array.isArray(cookie) ? cookie.join('; ') : cookie
};

for (const t of toLowLimit) {
try {
const payload = new URLSearchParams();
payload.append('hashes', t.hash);
payload.append('limit', String(config.lowUploadLimit));

await axios.post(
`${qb.url}/api/v2/torrents/setUploadLimit`,
payload.toString(),
{ headers: requestHeaders }
);

const ratio = (Number(t.uploaded || 0) / Number(t.size || 1)).toFixed(2);
const uploadedGiB = (Number(t.uploaded || 0) / 1024 / 1024 / 1024).toFixed(2);

logger.info(
`[${qb.name}] 已切低速: ${t.name} | 比例: ${ratio}x | 已传: ${uploadedGiB} GiB | 限速: ${config.lowUploadLimitText}`
);
} catch (e) {
logger.info(`[${qb.name}] 切低速失败: ${t.name} | ${e.message}`);
}
}

for (const t of toHighLimit) {
try {
const payload = new URLSearchParams();
payload.append('hashes', t.hash);
payload.append('limit', String(config.highUploadLimit));

await axios.post(
`${qb.url}/api/v2/torrents/setUploadLimit`,
payload.toString(),
{ headers: requestHeaders }
);

const ratio = (Number(t.uploaded || 0) / Number(t.size || 1)).toFixed(2);
const uploadedGiB = (Number(t.uploaded || 0) / 1024 / 1024 / 1024).toFixed(2);

logger.info(
`[${qb.name}] 已切高速: ${t.name} | 比例: ${ratio}x | 已传: ${uploadedGiB} GiB | 限速: ${config.highUploadLimitText}`
);
} catch (e) {
logger.info(`[${qb.name}] 切高速失败: ${t.name} | ${e.message}`);
}
}
} catch (e) {
logger.info(`[${qb.name}] 节点执行异常: ${e.message}`);
}
}
};

二、删种规则


以下规则用于处理下载异常、低回报做种和磁盘空间压力。多条规则同时使用时,请在 Vertex 中确认优先级和执行顺序。


下载阶段考察分享率



  • 类型:javascript

  • 优先级:90


完整源码:


(maindata, torrent) => {
/**
* Vertex 规则:
* 下载阶段按“分享率 = uploaded / downloaded”分段判断。
*
* 命中以下任一条件时返回 true:
* - 下载进度 >= 10%,且分享率 < 0.05
* - 下载进度 >= 30%,且分享率 < 0.09
* - 下载进度 >= 50%,且分享率 < 0.25
*/


const config = {
/**
* 只处理下载中的任务。
*/

targetStates: ['downloading', 'stalledDL'],

/**
* 永不处理的分类。
*/

keepCategories: ['keep'],

/**
* 新任务保护时间,单位:秒。
*
* 给任务一点时间建立连接和上传机会。
* 当前为 15 分钟。
*/

protectSeconds: 900,

/**
* 接近完成时不再处理。
*/

maxProgress: 0.95,

/**
* 分阶段分享率阈值。
*
* 分享率 = uploaded / downloaded
*/

ratioStages: [
{ minProgress: 0.10, maxRatio: 0.05 },
{ minProgress: 0.30, maxRatio: 0.09 },
{ minProgress: 0.50, maxRatio: 0.25 }
]
};

function isKeepCategory(t) {
const category = t.category || '';
return config.keepCategories.indexOf(category) !== -1;
}

function isTargetState(t) {
const state = t.state || '';
return config.targetStates.indexOf(state) !== -1;
}

if (isKeepCategory(torrent)) {
return false;
}

if (!isTargetState(torrent)) {
return false;
}

const now = Number(maindata?.now || Math.floor(Date.now() / 1000));
const addedTime = Number(torrent.addedTime || 0);
const progress = Number(torrent.progress || 0);
const downloaded = Number(torrent.downloaded || 0);
const uploaded = Number(torrent.uploaded || 0);

/**
* 新任务保护。
*/

if (addedTime > 0 && now - addedTime < config.protectSeconds) {
return false;
}

/**
* 非有效下载阶段直接跳过。
*/

if (progress <= 0 || progress >= config.maxProgress) {
return false;
}

/**
* 没有实际下载量时无法计算分享率。
*/

if (downloaded <= 0) {
return false;
}

const ratio = uploaded / downloaded;

for (const stage of config.ratioStages) {
if (progress >= stage.minProgress && ratio < stage.maxRatio) {
return true;
}
}

return false;
};

空间压力小于30GB



  • 类型:javascript

  • 优先级:110


完整源码:


(maindata, torrent) => {
/**
* Vertex 删种规则:
* 空间不足时,删除“历史平均上传速度最低”的 1 个种子。
*
* 历史平均上传速度 = 已上传量 uploaded / 做种时长
*
* 只使用 Vertex 官方文档里明确存在的字段:
* - maindata.freeSpaceOnDisk
* - maindata.torrents
* - torrent.name
* - torrent.size
* - torrent.progress
* - torrent.uploaded
* - torrent.category
* - torrent.state
* - torrent.completedTime
*/


const config = {
/**
* 剩余空间低于这个值时,开始进入删种判断。
* 当前阈值:30 GiB。
*/

freeSpaceTrigger: 30 * 1024 ** 3,

/**
* 新完成种子的保护时间。
*
* 当前为 30 分钟。
* 刚完成的种子 uploaded 可能还没增长,
* 平均上传速度会天然很低,所以需要保护。
*/

protectSeconds: 0.5 * 3600,

/**
* 永不删除的分类。
*
* 你可以在 qBittorrent / Vertex 里给重要种子设置这些分类。
*/

keepCategories: ['keep', 'KEEP'],

/**
* 参与删种排序的状态。
*
* uploading = 正在做种并上传
* stalledUP = 做种但当前无上传
* queuedUP = 排队等待上传
* forcedUP = 强制做种
* pausedUP = 已完成但暂停
*
* checkingUP 没放进来,是为了避免校验中的种子被删。
*/

seedingStates: [
'uploading',
'stalledUP',
'queuedUP',
'forcedUP',
'pausedUP'
],

/**
* 最少候选数量。
*
* 候选数量小于这个值时不删。
* 这样可以避免只剩 1 个可删种子时还继续删。
*
* 候选种子少于 2 个时不删除。
*/

minCandidateCount: 2
};

/**
* 当前时间,单位:秒。
*
* Vertex 官方示例中使用 moment().unix()。
*/

const now = moment().unix();

/**
* 当前剩余空间,单位:Byte。
*/

const freeSpace = Number(maindata.freeSpaceOnDisk || 0);

/**
* 空间没有压力,直接不删。
*
* 所以这个规则不会无脑删种:
* 只有 freeSpace <= freeSpaceTrigger 时才会继续往下判断。
*/

if (freeSpace > config.freeSpaceTrigger) {
return false;
}

/**
* 判断是否为白名单分类。
*/

function isKeepCategory(t) {
const category = t.category || '';
return config.keepCategories.indexOf(category) !== -1;
}

/**
* 判断种子是否已完成。
*
* progress >= 1 表示 100% 完成。
*/

function isCompleted(t) {
return Number(t.progress || 0) >= 1;
}

/**
* 判断是否为做种相关状态。
*/

function isSeedingState(t) {
const state = t.state || '';
return config.seedingStates.indexOf(state) !== -1;
}

/**
* 计算完成后的做种时长,单位:秒。
*
* completedTime 是完成时间戳。
* 如果 completedTime <= 0,说明没有有效完成时间。
*/

function seedAgeSeconds(t) {
const completedTime = Number(t.completedTime || 0);

if (completedTime <= 0) {
return 0;
}

return Math.max(now - completedTime, 0);
}

/**
* 计算历史平均上传速度,单位:Byte/s。
*
* 公式:
* 历史平均上传速度 = uploaded / 做种时长
*
* uploaded 是总上传量,单位 Byte。
*/

function avgUploadSpeed(t) {
const age = seedAgeSeconds(t);

if (age <= 0) {
return 0;
}

return Number(t.uploaded || 0) / age;
}

/**
* 判断某个种子是否进入“候选池”。
*
* 只有满足以下条件才会参与排序:
* 1. 不是白名单分类
* 2. 已完成
* 3. 是做种相关状态
* 4. 完成时间超过保护期
*/

function isCandidate(t) {
if (isKeepCategory(t)) {
return false;
}

if (!isCompleted(t)) {
return false;
}

if (!isSeedingState(t)) {
return false;
}

if (seedAgeSeconds(t) < config.protectSeconds) {
return false;
}

return true;
}

/**
* 从全局种子列表中筛选候选种子。
*
* maindata.torrents 是 Vertex 文档中明确提供的种子列表。
*/

const candidates = (maindata.torrents || []).filter(isCandidate);

/**
* 候选数量太少,不删。
*/

if (candidates.length < config.minCandidateCount) {
return false;
}

/**
* 按历史平均上传速度从低到高排序。
*
* 平均速度越低,说明过去越低效。
*
* 如果平均速度相同:
* 优先删除 size 更大的种子,因为能释放更多空间。
*/

candidates.sort((a, b) => {
const avgA = avgUploadSpeed(a);
const avgB = avgUploadSpeed(b);

if (avgA !== avgB) {
return avgA - avgB;
}

return Number(b.size || 0) - Number(a.size || 0);
});

/**
* 候选池中历史平均上传速度最低的种子。
*/

const worst = candidates[0];

/**
* Vertex 会对每个 torrent 调用一次这个函数。
*
* 只有当前 torrent 正好是 worst 时,才返回 true 删除。
*
* 文档里没有 hash 字段,所以这里用:
* - name
* - size
* - completedTime
*
* 三个字段一起匹配,降低重名种子误删概率。
*/

return (
torrent.name === worst.name &&
Number(torrent.size || 0) === Number(worst.size || 0) &&
Number(torrent.completedTime || 0) === Number(worst.completedTime || 0)
);
};

黑盒-做种回报差-3倍跳车



  • 类型:javascript

  • 优先级:40


完整源码:


(maindata, torrent) => {
const config = {
speedThreshold: 1 * 1024 * 1024, // 平均上传速度阈值:1 MiB/s
keepCategories: ['keep', 'KEEP'],
protectSeconds: 300, // 新完成种子保护 5 分钟 // 新完成种子保护 5 分钟
seedingStates: [
'uploading',
'stalledUP',
'queuedUP',
'forcedUP',
'pausedUP'
],
ratioDeleteThreshold: 3,

// 为空表示不过滤;填关键字可只看某个种子
debugNameKeyword: ''
};

const now = moment().unix();

const isKeep = (t) =>
config.keepCategories.includes(t.category || '');

const isCompleted = (t) =>
Number(t.progress || 0) >= 1;

const isSeeding = (t) =>
config.seedingStates.includes(t.state || '');

const age = (t) => {
const completedTime = Number(t.completedTime || 0);
return completedTime > 0 ? now - completedTime : 0;
};

const avgSpeed = (t) => {
const seedAge = age(t);
if (seedAge <= 0) return 0;
return Number(t.uploaded || 0) / seedAge;
};

const ratioField = (t) =>
Number(t.ratio || 0);

const uploadSizeRatio = (t) => {
const size = Number(t.size || 0);
const uploaded = Number(t.uploaded || 0);
if (size <= 0) return 0;
return uploaded / size;
};

const shouldDebug = (t) => {
if (!config.debugNameKeyword) return true;
return String(t.name || '').includes(config.debugNameKeyword);
};

const log = (t, stage, reason) => {
if (!shouldDebug(t)) return;

logger.info([
`[删种调试] ${stage}`,
`name=${t.name || '-'}`,
`state=${t.state || '-'}`,
`progress=${Number(t.progress || 0).toFixed(4)}`,
`ratioField=${ratioField(t).toFixed(4)}`,
`uploadSizeRatio=${uploadSizeRatio(t).toFixed(4)}`,
`uploadSpeed=${Number(t.uploadSpeed || 0)}`,
`age=${age(t)}`,
`avgSpeed=${avgSpeed(t).toFixed(2)}`,
`reason=${reason}`
].join(' | '));
};

if (isKeep(torrent)) {
return false;
}

if (!isCompleted(torrent)) {
return false;
}

if (!isSeeding(torrent)) {
return false;
}

if (ratioField(torrent) >= config.ratioDeleteThreshold) {
log(torrent, 'delete', 'ratio-field>=3');
return true;
}

if (age(torrent) < config.protectSeconds) {
log(torrent, 'skip', 'protect-seconds');
return false;
}

if (Number(torrent.uploadSpeed || 0) > config.speedThreshold) {
log(torrent, 'skip', 'active-uploading');
return false;
}

if (avgSpeed(torrent) < config.speedThreshold) {
log(torrent, 'delete', 'avg-speed-low');
return true;
}

log(torrent, 'skip', 'avg-speed-not-low');
return false;
};

长时间不发车删



  • 类型:normal

  • 优先级:70

  • 持续时间(秒):1800

  • 删除数量:1


完整源码:


(maindata, torrent) => {
return false;
}

下载错误



  • 类型:normal

  • 优先级:100

  • 持续时间(秒):300

  • 仅删除种子记录:False


完整源码:


(maindata, torrent) => {
return false;
}

慢车



  • 类型:javascript

  • 优先级:90

  • 持续时间(秒):60


完整源码:


(maindata, torrent) => {
/**
* Vertex 规则:
* 在特定环境下,找出“下载中、上传偏低、且已开始稳定下载”的种子。
*
* 命中条件:
* 1. 不属于保留分类
* 2. 不在凌晨保护时段
* 3. 全局 leechingCount 大于 10
* 4. 当前种子 leecher 小于 100
* 5. 状态为 downloading
* 6. 上传速度 <= 550 KiB/s
* 7. 下载进度 >= 10%
* 8. 加种时间 >= 600 秒
*/


const config = {
/**
* 永不处理的分类。
*/

keepCategories: ['keep'],

/**
* 只处理下载中的种子。
*/

targetState: 'downloading',

/**
* 凌晨保护时段:
* 00:00 - 07:59 不处理。
*/

quietHoursStart: 0,
quietHoursEnd: 7,

/**
* 全局下载数量保护阈值。
*/

minLeechingCount: 10,

/**
* 热门种子保护阈值。
*/

maxLeecher: 100,

/**
* 上传速度上限。
*/

maxUploadSpeed: util.calSize(550, 'KiB'),

/**
* 最低下载进度。
*/

minProgress: 0.1,

/**
* 新加种子的最小存活时间,单位:秒。
*/

minAgeSeconds: 600
};

const currentHour = moment().hour();
const now = moment().unix();

const state = torrent.state || '';
const category = torrent.category || '';
const leecher = Number(torrent.leecher || 0);
const uploadSpeed = Number(torrent.uploadSpeed || 0);
const progress = Number(torrent.progress || 0);
const addedTime = Number(torrent.addedTime || 0);
const leechingCount = Number(maindata.leechingCount || 0);

/**
* 保留分类直接跳过。
*/

if (config.keepCategories.indexOf(category) !== -1) {
return false;
}

/**
* 热门种子不处理。
*/

if (leecher >= config.maxLeecher) {
return false;
}

/**
* 凌晨保护时段直接跳过。
*/

if (
currentHour >= config.quietHoursStart &&
currentHour <= config.quietHoursEnd
) {
return false;
}

/**
* 全局下载数太少时不处理。
*/

if (leechingCount <= config.minLeechingCount) {
return false;
}

/**
* 新加种子先保护一段时间。
*/

if (addedTime > 0 && now - addedTime < config.minAgeSeconds) {
return false;
}

/**
* 主规则:
* 下载中、上传偏低、且进度达到阈值时命中。
*/

return (
state === config.targetState &&
uploadSpeed <= config.maxUploadSpeed &&
progress >= config.minProgress
);
};

白盒-做种回报差



  • 类型:javascript

  • 优先级:40


完整源码:


(maindata, torrent) => {

const config = {

speedThreshold: 1.5 * 1024 * 1024, // 1.5 MiB/s

keepCategories: ['keep', 'KEEP'],

protectSeconds: 900, // 新完成种子保护 15 分钟

// 做种超过 6 小时后进入强规则判断
maxSeedingSeconds: 6 * 3600,

seedingStates: [
'uploading',
'stalledUP',
'queuedUP',
'forcedUP',
'pausedUP'
]
};

const now = moment().unix();

const isKeep = (t) =>
config.keepCategories.includes(t.category || '');

const isCompleted = (t) =>
Number(t.progress || 0) >= 1;

const isSeeding = (t) =>
config.seedingStates.includes(t.state || '');

const age = (t) => {
const c = Number(t.completedTime || 0);
return c > 0 ? now - c : 0;
};

const avgSpeed = (t) => {
const a = age(t);
if (a <= 0) return 0;
return Number(t.uploaded || 0) / a;
};

const isActiveUploading = (t) =>
Number(t.uploadSpeed || 0) > 1 * 1024 * 1024;

const shouldDelete = (t) => {

// 1. 白名单永不删除
if (isKeep(t)) return false;

// 2. 未完成不删
if (!isCompleted(t)) return false;

// 3. 不是做种状态不处理
if (!isSeeding(t)) return false;

const tAge = age(t);

// 4. 新种保护
if (tAge < config.protectSeconds) return false;

// 5. 做种超过6小时:直接删除(强规则)
if (tAge >= config.maxSeedingSeconds) return true;

// 6. 实时高活跃上传保护
if (isActiveUploading(t)) return false;

// 7. 历史平均上传效率判断
return avgSpeed(t) < config.speedThreshold;
};

return shouldDelete(torrent);
};

黑种-3倍下载/上传



  • 类型:javascript

  • 优先级:100

  • 持续时间(秒):60


完整源码:


(maindata, torrent) => {
/**
* Vertex 下载中删种规则:
* 1. 只处理下载中的任务
* 2. 新种保护期内不删,等待 Peer 连接稳定
* 3. 快完成时不删,避免收尾误删
* 4. 下载速度太低时不删
* 5. 上传速度已经不错时不删
* 6. 下载速度明显大于上传速度时删除
*/


const config = {
protectSeconds: 3 * 60, // 新任务保护 3 分钟
completeProtectProgress: 0.95,
minDownloadSpeed: 5 * 1024 ** 2, // 最低下载速度:5 MiB/s
minUploadSpeed: 2 * 1024 ** 2, // 上传保护阈值:2 MiB/s
badSpeedRatio: 3 // 下载速度必须超过上传速度 3 倍
};

const now = moment().unix();
const addedTime = Number(torrent.addedTime || 0);
const progress = Number(torrent.progress || 0);
const downSpeed = Number(torrent.downloadSpeed || 0);
const upSpeed = Number(torrent.uploadSpeed || 0);

if (torrent.state !== 'downloading') {
return false;
}

if (addedTime > 0 && now - addedTime < config.protectSeconds) {
return false;
}

if (progress > config.completeProtectProgress) {
return false;
}

if (downSpeed < config.minDownloadSpeed) {
return false;
}

if (upSpeed > config.minUploadSpeed) {
return false;
}

if ((upSpeed > 0 ? downSpeed / upSpeed : Infinity) > config.badSpeedRatio) {
return true;
}

return false;
};

空间压力小于10GB



  • 类型:javascript

  • 优先级:110


完整源码:


(maindata, torrent) => {
/**
* Vertex 删种规则:
* 空间不足时,删除“历史平均上传速度最低”的 1 个种子。
*
* 历史平均上传速度 = 已上传量 uploaded / 做种时长
*
* 只使用 Vertex 官方文档里明确存在的字段:
* - maindata.freeSpaceOnDisk
* - maindata.torrents
* - torrent.name
* - torrent.size
* - torrent.progress
* - torrent.uploaded
* - torrent.category
* - torrent.state
* - torrent.completedTime
*/


const config = {
/**
* 剩余空间低于这个值时,开始进入删种判断。
* 当前阈值:10 GiB。
*/

freeSpaceTrigger: 10 * 1024 ** 3,

/**
* 新完成种子的保护时间。
*
* 当前为 30 分钟。
* 刚完成的种子 uploaded 可能还没增长,
* 平均上传速度会天然很低,所以需要保护。
*/

protectSeconds: 0.5 * 3600,

/**
* 永不删除的分类。
*
* 你可以在 qBittorrent / Vertex 里给重要种子设置这些分类。
*/

keepCategories: ['keep', 'KEEP'],

/**
* 参与删种排序的状态。
*
* uploading = 正在做种并上传
* stalledUP = 做种但当前无上传
* queuedUP = 排队等待上传
* forcedUP = 强制做种
* pausedUP = 已完成但暂停
*
* checkingUP 没放进来,是为了避免校验中的种子被删。
*/

seedingStates: [
'uploading',
'stalledUP',
'queuedUP',
'forcedUP',
'pausedUP'
],

/**
* 最少候选数量。
*
* 候选数量小于这个值时不删。
* 这样可以避免只剩 1 个可删种子时还继续删。
*
* 候选种子少于 2 个时不删除。
*/

minCandidateCount: 2
};

/**
* 当前时间,单位:秒。
*
* Vertex 官方示例中使用 moment().unix()。
*/

const now = moment().unix();

/**
* 当前剩余空间,单位:Byte。
*/

const freeSpace = Number(maindata.freeSpaceOnDisk || 0);

/**
* 空间没有压力,直接不删。
*
* 所以这个规则不会无脑删种:
* 只有 freeSpace <= freeSpaceTrigger 时才会继续往下判断。
*/

if (freeSpace > config.freeSpaceTrigger) {
return false;
}

/**
* 判断是否为白名单分类。
*/

function isKeepCategory(t) {
const category = t.category || '';
return config.keepCategories.indexOf(category) !== -1;
}

/**
* 判断种子是否已完成。
*
* progress >= 1 表示 100% 完成。
*/

function isCompleted(t) {
return Number(t.progress || 0) >= 1;
}

/**
* 判断是否为做种相关状态。
*/

function isSeedingState(t) {
const state = t.state || '';
return config.seedingStates.indexOf(state) !== -1;
}

/**
* 计算完成后的做种时长,单位:秒。
*
* completedTime 是完成时间戳。
* 如果 completedTime <= 0,说明没有有效完成时间。
*/

function seedAgeSeconds(t) {
const completedTime = Number(t.completedTime || 0);

if (completedTime <= 0) {
return 0;
}

return Math.max(now - completedTime, 0);
}

/**
* 计算历史平均上传速度,单位:Byte/s。
*
* 公式:
* 历史平均上传速度 = uploaded / 做种时长
*
* uploaded 是总上传量,单位 Byte。
*/

function avgUploadSpeed(t) {
const age = seedAgeSeconds(t);

if (age <= 0) {
return 0;
}

return Number(t.uploaded || 0) / age;
}

/**
* 判断某个种子是否进入“候选池”。
*
* 只有满足以下条件才会参与排序:
* 1. 不是白名单分类
* 2. 已完成
* 3. 是做种相关状态
* 4. 完成时间超过保护期
*/

function isCandidate(t) {
if (isKeepCategory(t)) {
return false;
}

if (!isCompleted(t)) {
return false;
}

if (!isSeedingState(t)) {
return false;
}

if (seedAgeSeconds(t) < config.protectSeconds) {
return false;
}

return true;
}

/**
* 从全局种子列表中筛选候选种子。
*
* maindata.torrents 是 Vertex 文档中明确提供的种子列表。
*/

const candidates = (maindata.torrents || []).filter(isCandidate);

/**
* 候选数量太少,不删。
*/

if (candidates.length < config.minCandidateCount) {
return false;
}

/**
* 按历史平均上传速度从低到高排序。
*
* 平均速度越低,说明过去越低效。
*
* 如果平均速度相同:
* 优先删除 size 更大的种子,因为能释放更多空间。
*/

candidates.sort((a, b) => {
const avgA = avgUploadSpeed(a);
const avgB = avgUploadSpeed(b);

if (avgA !== avgB) {
return avgA - avgB;
}

return Number(b.size || 0) - Number(a.size || 0);
});

/**
* 候选池中历史平均上传速度最低的种子。
*/

const worst = candidates[0];

/**
* Vertex 会对每个 torrent 调用一次这个函数。
*
* 只有当前 torrent 正好是 worst 时,才返回 true 删除。
*
* 文档里没有 hash 字段,所以这里用:
* - name
* - size
* - completedTime
*
* 三个字段一起匹配,降低重名种子误删概率。
*/

return (
torrent.name === worst.name &&
Number(torrent.size || 0) === Number(worst.size || 0) &&
Number(torrent.completedTime || 0) === Number(worst.completedTime || 0)
);
};

三、RSS 规则


RSS 规则负责控制自动加入下载器的任务,建议根据自己的磁盘容量和下载带宽调整限制。


rss-allow



  • 类型:javascript

  • 优先级:12


完整源码:


(torrent) => {
const config = {
downloaderId: '1b55ff1e',
enableLogging: false,

// 单个种子体积范围
// 小于 800 MiB 的种子通常刷流效率偏低
// 大于 60 GiB 的种子不自动加入
minTorrentSize: 800 * 1024 * 1024, // 800 MiB
maxTorrentSize: 60 * 1024 * 1024 * 1024, // 60 GiB

// 当前下载器所有任务的体积上限
maxHoldingSize: 85 * 1024 * 1024 * 1024, // 85 GiB

// 未完成任务的总体积上限
maxIncompleteSize: 60 * 1024 * 1024 * 1024, // 60 GiB

// 下载任务上限
// 同时下载任务数量上限
maxDownloadingTaskCount: 5
};

const fs = require('fs');
const path = require('path');

function logMessage(message) {
if (!config.enableLogging) return;

const logFilePath = '/vertex/log22/log.txt';
const logDir = path.dirname(logFilePath);

if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}

fs.appendFileSync(
logFilePath,
`[${new Date().toLocaleString('zh-CN', { hour12: false })}] ${message}\n`
);
}

// Vertex 运行时里,当前下载器对象从 global.runningClient 取
const client = global.runningClient[config.downloaderId];
if (!client || !client.maindata || !client.maindata.torrents) {
logMessage(`未找到下载器或 maindata 不可用: ${config.downloaderId}`);
return false;
}

const torrentSize = Number(torrent.size || 0);

// 1. 先控制单个种子的大小
if (torrentSize < config.minTorrentSize || torrentSize > config.maxTorrentSize) {
logMessage(`拒绝 ${torrent.name}:体积不在允许范围内`);
return false;
}

let holdingSize = 0;
let incompleteSize = 0;
let downloadingTaskCount = 0;

// 2. 统计当前下载器总体状态
for (const existingTorrent of client.maindata.torrents) {
const size = Number(existingTorrent.size || 0);
const progress = Number(existingTorrent.progress || 0);
const state = existingTorrent.state || '';

// 当前下载器总持有量
holdingSize += size;

// 未完成任务总量
if (progress < 1) {
incompleteSize += size;
}

// 下载任务数
if (
state === 'downloading' ||
state === 'stalledDL' ||
state === 'metaDL' ||
state === 'forcedDL'
) {
downloadingTaskCount += 1;
}
}

// 3. 控制总持有量
if ((holdingSize + torrentSize) > config.maxHoldingSize) {
logMessage(`拒绝 ${torrent.name}:总持有体积超限`);
return false;
}

// 4. 控制未完成任务总体积
if ((incompleteSize + torrentSize) > config.maxIncompleteSize) {
logMessage(`拒绝 ${torrent.name}:未完成任务总体积超限`);
return false;
}

// 5. 控制下载任务数量
if (downloadingTaskCount >= config.maxDownloadingTaskCount) {
logMessage(`拒绝 ${torrent.name}:下载任务数 ${downloadingTaskCount} >= ${config.maxDownloadingTaskCount}`);
return false;
}

// 全部通过,允许进入下载器
return true;
};

四、使用前检查与 AI 优化建议



  1. 先确认 Vertex 支持的字段、运行时和下载器接口。

  2. 将脚本中的下载器地址、用户名、密码、下载器 ID 和保存路径替换成自己的值。

  3. 把 VPS 的 CPU、内存、磁盘剩余空间、网络上行、下载器版本和刷流目标交给 AI 分析。

  4. 让 AI 重点检查连接数、并发下载数、磁盘写入压力、空间阈值和删种保护时间。

  5. 首次使用时先观察日志,不要一次启用所有自动删种规则。

  6. 给重要种子设置 keepKEEP 分类。

  7. 涉及自动删除数据的规则,启用前先备份规则和重要文件。


可以把下面这段话发给 AI:


这是我的 Vertex/qBittorrent 刷流规则。请根据我的 VPS 配置、CPU、内存、磁盘容量与剩余空间、上下行带宽、下载器版本、站点规则和刷流目标,检查连接数、并发数、上传限速、磁盘空间阈值、保护时间和自动删种条件。请指出可能误删的地方,并给出修改后的完整脚本。不要凭空增加依赖,也不要删除 keep/KEEP 白名单逻辑。
最新回复 (0)
    没有回复
* 帖子来源NodeSeek
返回