- MarkXiu-nodeseek2026-09-0365784
Vertex 刷流规则脚本合集
本文整理自自己实际 Vertex 配置,纯粹是抛砖引玉,大佬轻喷。
这些脚本用于 qBittorrent 的自动删种、RSS 筛选和上传限速,分享出来供参考。不同 VPS 的 CPU、内存、磁盘容量、网络带宽、下载器版本和刷流目标不同,不建议直接照搬参数。
复制前,建议把自己的 VPS 配置和刷流目标交给 AI,让 AI 检查连接数、并发下载数、磁盘空间阈值、上传速度和删种条件。
重要资源建议设置为
keep或KEEP分类,并先使用测试任务验证。一、定时脚本
定时脚本按固定周期执行,适合做上传限速、标签处理等周期性任务。
黑盒-三倍限速(主要防止上传太猛, 下载完成已经超过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;
};- 类型:`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) ); };- 类型:
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;
};- 类型:`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 优化建议 - 先确认 Vertex 支持的字段、运行时和下载器接口。 - 将脚本中的下载器地址、用户名、密码、下载器 ID 和保存路径替换成自己的值。 - 把 VPS 的 CPU、内存、磁盘剩余空间、网络上行、下载器版本和刷流目标交给 AI 分析。 - 让 AI 重点检查连接数、并发下载数、磁盘写入压力、空间阈值和删种保护时间。 - 首次使用时先观察日志,不要一次启用所有自动删种规则。 - 给重要种子设置 `keep` 或 `KEEP` 分类。 - 涉及自动删除数据的规则,启用前先备份规则和重要文件。 可以把下面这段话发给 AI:``` 这是我的 Vertex/qBittorrent 刷流规则。请根据我的 VPS 配置、CPU、内存、磁盘容量与剩余空间、上下行带宽、下载器版本、站点规则和刷流目标,检查连接数、并发数、上传限速、磁盘空间阈值、保护时间和自动删种条件。请指出可能误删的地方,并给出修改后的完整脚本。不要凭空增加依赖,也不要删除 keep/KEEP 白名单逻辑。主楼 - 暂无回复