logo NodeSeekbeta

PT 刷流 Vertex 刷流规则分享

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 白名单逻辑。

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有71716位seeker

🎉欢迎新用户🎉