Skip to content

Local Agent API

Whistle 的本地 API,供 AI Agent 或第三方进程以编程方式操作 Whistle 代理服务。支持规则管理、插件控制、抓包数据获取、HTTPS 代理开关、证书管理等核心功能,具体用法如下(确保已全局安装 Whistle 且版本 ≥ 2.10.7):

javascript
const cp = require('child_process');
const qs = require('querystring');

const loadModule = require;
let w2Api;

/**
 * @async 异步加载 Whistle API 模块
 * @function loadApi
 * @description
 * 尝试加载 Whistle 的 API 模块,并校验其版本是否符合最低要求(≥ 2.10.7)。
 * 如果版本过低,或加载过程中发生任何错误(如子进程调用失败、模块加载异常、网络状态获取失败等),
 * 都会抛出对应的错误。
 * 
 * @returns {Promise<Object>} 返回 Whistle API 模块对象(通常包含 network、rules 等接口)
 * 
 * @throws {Error} 当执行 `w2 root bin/api` 命令失败时抛出(例如 whistle 未安装或未在 PATH 中,需要通过 `npm install -g whistle` 安装)
 * @throws {Error} 当调用 `api.network.getStatus()` 失败时抛出(例如 whistle 或 whistle client 服务未启动)
 * @throws {Error} 当 Whistle 版本低于 2.10.7 时抛出,错误信息为 'Whistle version must be >= 2.10.7'
 * 
 * @example
 * try {
 *   const api = await loadApi();
 *   // 使用 api.network.getStatus() 等
 * } catch (err) {
 *   console.error('加载 Whistle API 失败:', err.message);
 * }
 */
const loadApi = async () => {
  if (!w2Api) {
    w2Api = loadModule(cp.spawnSync('w2', ['root', 'bin/api']).stdout.toString().trim());
  }
  const { version } = await w2Api.network.getStatus();
  const [major, minor, patch] = version.split('.').map(Number);
  w2Api.version = version;
  if (major > 2 || (major == 2 && (minor > 10 || (minor == 10 && patch >= 7)))) {
    return w2Api;
  }
  throw new Error('Whistle version must be >= 2.10.7');
};

/**
 * @abstract 判断当前 Whistle 是否启用了 HTTPS 代理
 * @function isEnabledHTTPS
 * @description
 * 通过 Whistle API 检查当前代理是否启用了 HTTPS 代理功能。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @returns {Promise<boolean>} 当前是否启用了 HTTPS 代理
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.isEnabledHTTPS()` 调用失败
 *
 * @example
 * try {
 *   const enabled = await isEnabledHTTPS();
 *   console.log(`当前 HTTPS 代理是否启用: ${enabled}`);
 * } catch (err) {
 *   console.error('检查 HTTPS 代理状态失败:', err.message);
 * }
 */
const isEnabledHTTPS = async () => {
  const api = await loadApi();
  return await api.isEnabledHTTPS();
};

/**
 * @async 设置 Whistle 是否启用 HTTPS 代理
 * @function setEnableHTTPS
 * @description
 * 通过 Whistle API 设置当前代理是否启用 HTTPS 代理功能。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @param {boolean} enable - 是否启用 HTTPS 代理
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.setEnableHTTPS()` 调用失败
 *
 * @example
 * try {
 *   await setEnableHTTPS(true);
 *   console.log('设置 HTTPS 代理启用状态成功');
 * } catch (err) {
 *   console.error('设置 HTTPS 代理状态失败:', err.message);
 * }
 */
const setEnableHTTPS = async (enable) => {
  const api = await loadApi();
  await api.setEnableHTTPS(enable);
};

/**
 * @async 获取 Whistle 的根证书信息
 * @function getRootCA
 * @description
 * 通过 Whistle API 获取当前代理的根证书信息,包括证书内容、私钥等。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @returns {Promise<Buffer>} 返回根证书的 Buffer 对象,可用于保存为 .crt 文件或进行其他操作
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.getRootCA()` 调用失败
 */
const getRootCA = async () => {
  const api = await loadApi();
  return await api.getRootCA();
};

/**
 * @async 创建一个临时文件并写入指定内容
 * @function createFile
 * @description
 * 通过 Whistle API 创建一个临时文件,并将指定的内容写入该文件。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @param {Buffer|string} data - 要创建的文件内容,可以是 Buffer 或字符串
 * @returns {Promise<string>} 返回创建的文件路径
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.createFile()` 调用失败
 *
 * @example
 * try {
 *   const filePath = await createFile('test');
 *   console.log(`临时文件已创建: ${filePath}`);
 *   // 临时文件已创建: temp/7d2e9a424672279e5bcaab948a7a63fd47cb381626555835e828438762f86a96
 * } catch (err) {
 *   console.error('创建临时文件失败:', err.message);
 * }
 */
const createFile = async (data) => {
  const api = await loadApi();
  return await api.createFile(data);
};

/**
 * @async 获取指定文件的内容
 * @function getFile
 * @description
 * 通过 Whistle API 获取指定路径的文件内容,返回其字符串内容。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @param {string} filePath - 要读取的文件路径
 * @returns {Promise<string>} 返回文件内容的字符串
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.getFile()` 调用失败(如文件不存在)
 */
const getFile = async (filePath) => {
  const api = await loadApi();
  return await api.getFile(filePath);
};

/**
 * @async 获取 Whistle 规则引擎的当前状态配置
 * @function getRulesStatus
 * @description
 * 通过 Whistle API 获取规则列表、启用状态以及优先级策略等运行时配置。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @returns {Promise<{
 *   laterRulesFirst: boolean,   // 是否启用“后置规则优先”策略(即后匹配的规则覆盖前匹配的)
 *   multiSelect: boolean,       // 是否启用多选模式
 *   pluginsDisabled: boolean,   // 是否全局禁用所有插件规则
 *   disabled: boolean,          // 是否全局禁用 Whistle 代理(所有规则失效)
 *   list: Array<{
 *     name: string,            // 规则文件/配置的名称
 *     selected: boolean        // 当前是否处于激活(勾选)状态
 *   }>
 * }>} 返回包含规则状态和列表的对象
 *
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.getStatus()` 调用失败
 *
 * @example
 * try {
 *   const { list, disabled, multiSelect, laterRulesFirst } = await getRulesStatus();
 *   const activeRules = list.filter(r => r.selected);
 *   console.log(`激活规则 (${activeRules.length}):`, activeRules.map(r => r.name));
 * } catch (err) {
 *   console.error('获取规则状态失败:', err.message);
 * }
 */
const getRulesStatus = async () => {
  const api = await loadApi();
  return await api.rules.getStatus();
};

/**
 * @async 关闭 Whistle 规则引擎
 * @function turnOffRules
 * @description
 * 通过 Whistle API 全局禁用所有规则(不包括插件规则)。
 * 该操作会立即生效,影响当前代理会话中的所有请求处理。
 * 依赖 `loadApi()` 加载 API 模块,若 Whistle 服务未运行或版本不兼容则会失败。
 *
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.turnOff()` 调用失败(如网络异常)
 *
 * @example
 * try {
 *   await turnOffRules();
 *   console.log('规则已关闭,所有请求将不再受 Whistle 规则影响');
 * } catch (err) {
 *   console.error('关闭规则失败:', err.message);
 *   // 可依据错误类型进行降级或重试
 * }
 */
const turnOffRules = async () => {
  const api = await loadApi();
  await api.rules.turnOff();
};

/**
 * @async 启用 Whistle 规则引擎
 * @function turnOnRules
 * @description
 * 通过 Whistle API 全局启用之前配置的所有规则(不包括插件的规则)。
 * 该操作会立即生效,恢复代理请求的规则处理逻辑。
 * 依赖 `loadApi()` 加载 API 模块,若 Whistle 服务未运行或版本不兼容则会失败。
 *
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.turnOn()` 调用失败(如网络异常)
 *
 * @example
 * try {
 *   await turnOnRules();
 *   console.log('规则已启用,请求将按配置规则处理');
 * } catch (err) {
 *   console.error('启用规则失败:', err.message);
 *   // 可依据错误类型进行降级或重试
 * }
 */
const turnOnRules = async () => {
  const api = await loadApi();
  await api.rules.turnOn();
};

/**
 * @async 选中(激活)指定的 Whistle 规则文件,并自动启用规则引擎
 * @function selectRule
 * @description
 * 调用 Whistle API 选中指定名称的规则文件(通常从规则列表中选择),
 * 若选中操作成功(返回 truthy 值),则自动调用 `turnOnRules()` 启用全局规则引擎,
 * 使该规则立即生效。该操作依赖 `loadApi()` 加载 API 模块。
 *
 * @param {string} ruleName - 要激活的规则文件名称(需与规则列表中的名称精确匹配)
 *
 * @returns {Promise<boolean>} 对应 ruleName 是否存在
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.select()` 调用失败(如网络异常)
 * @throws {Error} 若选中成功但启用规则引擎(`turnOnRules()`)失败时抛出
 *
 * @example
 * try {
 *   await selectRule('dev-rules');
 *   console.log('规则 "dev-rules" 已选中并启用');
 * } catch (err) {
 *   console.error('操作失败:', err.message);
 * }
 */
const selectRule = async (ruleName) => {
  const api = await loadApi();
  const result = await api.rules.select(ruleName);
  if (result) {
    await turnOnRules();
  }
  await result;
};

/**
 * @async 取消选中(停用)指定的 Whistle 规则文件
 * @function unselectRule
 * @description
 * 调用 Whistle API 取消选中指定名称的规则文件(通常从规则列表中选择),
 * 该操作依赖 `loadApi()` 加载 API 模块。
 *
 * @param {string} ruleName - 要取消激活的规则文件名称(需与规则列表中的名称精确匹配,如果 ruleName 不填则取消所有规则)
 *
 * @returns {Promise<boolean>} 对应 ruleName 是否存在
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.unselect()` 调用失败(如网络异常)
 *
 * @example
 * try {
 *   await unselectRule('dev-rules');
 *   console.log('规则 "dev-rules" 已取消选中');
 * } catch (err) {
 *   console.error('操作失败:', err.message);
 * }
 */
const unselectRule = async (ruleName) => {
  const api = await loadApi();
  const result = await api.rules.unselect(ruleName);
  await result;
};

/**
 * @async 获取指定规则文件的内容详情
 * @function getRule
 * @description
 * 通过 Whistle API 读取指定名称的规则文件内容,返回其详细配置信息。
 * 依赖 `loadApi()` 加载 API 模块。
 *
 * @param {string} ruleName - 规则文件的名称(需与 Whistle 规则列表中的名称精确匹配)
 *
 * @returns {Promise<{
 *   value: string,       // 规则内容(如 "* www.example.com 127.0.0.1")
 *   selected: boolean,     // 当前是否被选中
 * }>} 返回规则对象,具体字段可能因 Whistle 版本而异
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.get()` 调用失败(如规则名称不存在)
 *
 * @example
 * try {
 *   const rule = await getRule('my-rules');
 *   console.log(`规则的内容:\n${rule.value}`);
 *   if (rule.selected) {
 *     console.log('该规则当前已激活');
 *   }
 * } catch (err) {
 *   console.error('获取规则失败:', err.message);
 * }
 */
const getRule = async (ruleName) => {
  const api = await loadApi();
  return await api.rules.get(ruleName);
};

/**
 * @async 获取当前 Whistle 规则列表
 * @function getRuleList
 * @description
 * 通过 Whistle API 获取所有已配置的规则文件列表,包含每个规则的名称、内容及激活状态。
 * 依赖 `loadApi()` 加载 API 模块。
 *
 * @returns {Promise<Array<{
 *   name: string,      // 规则文件名称
 *   value: string,     // 规则内容(如 "* www.example.com 127.0.0.1")
 *   selected: boolean  // 当前是否被选中(激活)
 * }>>} 返回规则对象数组,若无可返回空数组
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.rules.getList()` 调用失败(如网络异常)
 *
 * @example
 * try {
 *   const rules = await getRuleList();
 *   console.log(`共 ${rules.length} 条规则`);
 *   const activeRules = rules.filter(r => r.selected);
 *   console.log(`激活规则 (${activeRules.length}):`, activeRules.map(r => r.name));
 * } catch (err) {
 *   console.error('获取规则列表失败:', err.message);
 * }
 */
const getRuleList = async () => {
  const api = await loadApi();
  return await api.rules.getList();
};

/**
 * @async 判断当前 Whistle 是否启用了多选模式
 * @function isMultiSelect
 * @description
 * 通过 Whistle API 检查当前规则引擎是否启用了多选模式。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @returns {Promise<boolean>} 当前是否启用了多选模式
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.isMultiSelect()` 调用失败
 *
 * @example
 * try {
 *   const multiSelect = await isMultiSelect();
 *   console.log(`当前规则引擎是否启用多选模式: ${multiSelect}`);
 * } catch (err) {
 *   console.error('检查多选模式状态失败:', err.message);
 * }
 */
const isMultiSelect = async () => {
  const api = await loadApi();
  return await api.rules.isMultiSelect();
};

/**
 * @async 设置 Whistle 规则引擎是否启用多选模式
 * @function setMultiSelect
 * @description
 * 通过 Whistle API 设置当前规则引擎是否启用了多选模式。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @param {boolean} enable - 是否启用多选模式
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.setMultiSelect()` 调用失败
 *
 * @example
 * try {
 *   await setMultiSelect(true);
 *   console.log('已启用多选模式');
 * } catch (err) {
 *   console.error('设置多选模式失败:', err.message);
 * }
 */
const setMultiSelect = async (enable) => {
  const api = await loadApi();
  await api.rules.setMultiSelect(enable);
};

/**
 * @async 设置 Whistle 规则引擎是否启用后置规则优先
 * @function setLaterRulesFirst
 * @description
 * 通过 Whistle API 设置当前规则引擎是否启用了后置规则优先。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {boolean} enable - 是否启用后置规则优先
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.setLaterRulesFirst()` 调用失败
 *
 * @example
 * try {
 *   await setLaterRulesFirst(true);
 *   console.log('已启用后置规则优先');
 * } catch (err) {
 *   console.error('设置后置规则优先失败:', err.message);
 * }
 */
const setLaterRulesFirst = async (enable) => {
  const api = await loadApi();
  await api.rules.setLaterFirst(enable);
};

/**
 * @async 添加一个新的规则文件到 Whistle 规则引擎中
 * @function addRule
 * @description
 * 通过 Whistle API 添加一个新的规则文件,并可选择是否立即选中(激活)该规则。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @param {string} name - 规则名称
 * @param {string} value - 规则内容
 * @param {boolean} selected - 是否选中
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.add()` 调用失败
 *
 * @example
 * try {
 *   await addRule('my-rule', '* www.example.com 127.0.0.1', true);
 *   console.log('已添加规则');
 * } catch (err) {
 *   console.error('添加规则失败:', err.message);
 * }
 */
const addRule = async (name, value, selected) => {
  const api = await loadApi();
  await api.rules.add(name, value, selected);
};

/**
 * @async 将指定规则文件移动到规则列表顶部
 * @function moveRuleToTop
 * @description
 * 通过 Whistle API 将指定规则文件移动到规则列表的顶部。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} ruleName - 规则文件的名称(需与规则列表中的名称精确匹配)
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.rules.moveToTop()` 调用失败
 *
 * @example
 * try {
 *   await moveRuleToTop('my-rule');
 *   console.log('已将规则移动到顶部');
 * } catch (err) {
 *   console.error('移动规则失败:', err.message);
 * }
 */
const moveRuleToTop = async (ruleName) => {
  const api = await loadApi();
  await api.rules.moveToTop(ruleName);
};

/**
 * @async 获取 Whistle 的 Values 列表
 * @function getValueList
 * @description
 * 通过 Whistle API 获取当前配置的 Values 列表,返回每个 Value 的名称和值。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 * 
 * @returns {Promise<Array<{ name: string, value: string }>>} 返回 Values 对象数组,若无可返回空数组
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.values.getList()` 调用失败
 *
 * @example
 * try {
 *   const values = await getValueList();
 *   console.log(`共 ${values.length} 个 Values`);
 *   values.forEach(v => console.log(`${v.name}: ${v.value}`));
 * } catch (err) {
 *   console.error('获取 Values 列表失败:', err.message);
 * }
 */
const getValueList = async () => {
  const api = await loadApi();
  return await api.values.getList();
};

/**
 * @async 获取指定的 Value
 * @function getValue
 * @description
 * 通过 Whistle API 获取指定的 Value。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} name - Value 的名称
 * @returns {Promise<{ name: string, value: string }>} 返回 Value 对象,若不存在返回 `null` 或 `undefined`
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.values.get()` 调用失败
 *
 * @example
 * try {
 *   const value = await getValue('my-value');
 *   console.log(`${value.name}: ${value.value}`);
 * } catch (err) {
 *   console.error('获取 Value 失败:', err.message);
 * }
 */
const getValue = async (name) => {
  const api = await loadApi();
  return await api.values.get(name);
};

/**
 * @async 添加一个新的 Value
 * @function addValue
 * @description
 * 通过 Whistle API 添加一个新的 Value。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} name - Value 的名称
 * @param {string} value - Value 的值
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.values.add()` 调用失败
 *
 * @example
 * try {
 *   await addValue('my-value', 'some-value');
 *   console.log('已添加 Value');
 * } catch (err) {
 *   console.error('添加 Value 失败:', err.message);
 * }
 */
const addValue = async (name, value) => {
  const api = await loadApi();
  await api.values.add(name, value);
};

/**
 * @async 获取 Whistle 插件的状态信息
 * @function getPluginsStatus
 * @description
 * 通过 Whistle API 获取当前已安装插件的状态信息,包括每个插件的名称、启用状态等。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @returns {Promise<Array<{ name: string, moduleName: string, version: string, selected: boolean }>>} 返回插件状态对象数组,若无可返回空数组
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.getStatus()` 调用失败
 * 
 * @example
 * try {
 *   const plugins = await getPluginsStatus();
 *   console.log(`共 ${plugins.length} 个插件`);
 *   plugins.forEach(p => console.log(`${p.name}: ${p.selected ? '启用' : '禁用'}`));
 * } catch (err) {
 *   console.error('获取插件状态失败:', err.message);
 * }
 */
const getPluginsStatus = async () => {
  const api = await loadApi();
  return await api.plugins.getStatus();
};

/**
 * @async 启用 Whistle 插件
 * @function turnOnPlugins
 * @description
 * 通过 Whistle API 启用所有已安装的插件。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.turnOn()` 调用失败
 * 
 * @example
 * try {
 *   await turnOnPlugins();
 *   console.log('已启用所有插件');
 * } catch (err) {
 *   console.error('启用插件失败:', err.message);
 * }
 */
const turnOnPlugins = async () => {
  const api = await loadApi();
  await api.plugins.turnOn();
};

/**
 * @async 禁用 Whistle 插件
 * @function turnOffPlugins
 * @description
 * 通过 Whistle API 禁用所有已安装的插件。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @returns {Promise<void>} 无返回值,成功时 Promise 解析为空
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.turnOff()` 调用失败
 * 
 * @example
 * try {
 *   await turnOffPlugins();
 *   console.log('已禁用所有插件');
 * } catch (err) {
 *   console.error('禁用插件失败:', err.message);
 * }
 */
const turnOffPlugins = async () => {
  const api = await loadApi();
  await api.plugins.turnOff();
};

/**
 * @async 获取 Whistle 插件列表
 * @function getPluginList
 * @description
 * 通过 Whistle API 获取当前已安装的所有插件信息,包括插件元数据、版本、描述、规则配置及激活状态等。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @returns {Promise<Array<{
 *   moduleName: string,          // 插件模块名(如 'whistle.inspect')
 *   name: string,                // 插件短名称(如 'inspect')
 *   mtime: number,               // 插件最后修改时间(Unix 毫秒时间戳)
 *   version: string,             // 插件版本号(如 '2.3.0')
 *   description: string,         // 插件描述信息
 *   homepage: string,            // 插件主页 URL
 *   rules: string,               // 自定义规则内容(可能为空)
 *   _rules: string,              // 内部规则表达式(通常包含动态替换逻辑)
 *   resRules: string,            // 响应规则内容(可能为空)
 *   selected: boolean            // 当前是否启用该插件
 * }>>} 返回插件对象数组,若无插件则返回空数组
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.getList()` 调用失败
 *
 * @example
 * try {
 *   const plugins = await getPluginList();
 *   console.log(`共 ${plugins.length} 个插件`);
 *   plugins.forEach(p => {
 *     console.log(`${p.name} (${p.version}): ${p.selected ? '启用' : '禁用'}`);
 *     console.log(`  描述: ${p.description}`);
 *   });
 * } catch (err) {
 *   console.error('获取插件列表失败:', err.message);
 * }
 */
const getPluginList = async () => {
  const api = await loadApi();
  return await api.plugins.getList();
};

/**
 * @async 获取指定的 Whistle 插件
 * @function getPlugin
 * @description
 * 通过 Whistle API 获取指定的插件信息。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} name - 插件的名称
 * @returns {Promise<{
 *   moduleName: string,          // 插件模块名(如 'whistle.inspect')
 *   name: string,                // 插件短名称(如 'inspect')
 *   mtime: number,               // 插件最后修改时间(Unix 毫秒时间戳)
 *   version: string,             // 插件版本号(如 '2.3.0')
 *   description: string,         // 插件描述信息
 *   homepage: string,            // 插件主页 URL
 *   rules: string,               // 自定义规则内容(可能为空)
 *   _rules: string,              // 内部规则表达式(通常包含动态替换逻辑)
 *   resRules: string,            // 响应规则内容(可能为空)
 *   selected: boolean            // 当前是否启用该插件
 * }>} 返回插件对象,若不存在返回 `null` 或 `undefined`
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.get()` 调用失败
 * 
 * @example
 * try {
 *   const plugin = await getPlugin('my-plugin');
 *   console.log(`${plugin.name}: ${plugin.enabled ? '启用' : '禁用'}`);
 * } catch (err) {
 *   console.error('获取插件失败:', err.message);
 * }
 */
const getPlugin = async (name) => {
  const api = await loadApi();
  return await api.plugins.get(name);
};

/**
 * @async 选择指定的 Whistle 插件
 * @function selectPlugin
 * @description
 * 通过 Whistle API 选择指定的插件。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} name - 插件的名称
 * @returns {Promise<boolean>} 返回选择结果,成功返回 `true`,失败返回 `false`
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.select()` 调用失败
 * 
 * @example
 * try {
 *   const result = await selectPlugin('my-plugin');
 *   if (result) {
 *     console.log('插件已选择并启用');
 *   } else {
 *     console.log('插件选择失败,可能不存在');
 *   }
 * } catch (err) {
 *   console.error('选择插件失败:', err.message);
 * }
 */
const selectPlugin = async (name) => {
  const api = await loadApi();
  const result = await api.plugins.select(name);
  if (result) {
    await turnOnPlugins();
  }
  return result;
};

/**
 * @async 取消选择指定的 Whistle 插件
 * @function unselectPlugin
 * @description
 * 通过 Whistle API 取消选择指定的插件。
 * 依赖 `loadApi()` 加载 API 模块,若加载失败或版本过低会抛出异常。
 *
 * @param {string} name - 插件的名称
 * @returns {Promise<boolean>} 返回取消选择结果,成功返回 `true`,失败返回 `false`
 * @throws {Error} Whistle API 加载失败(如 w2 命令不可用、模块加载错误)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未运行或 `api.plugins.unselect()` 调用失败
 * @example
 * try {
 *   const result = await unselectPlugin('my-plugin');
 *   if (result) {
 *     console.log('插件已取消选择并禁用');
 *   } else {
 *     console.log('插件取消选择失败,可能不存在');
 *   }
 * } catch (err) {
 *   console.error('取消选择插件失败:', err.message);
 * }
 */
const unselectPlugin = async (name) => {
  const api = await loadApi();
  return await api.plugins.unselect(name);
};

const URLENCODED_RE = /application\/x-www-form-urlencoded/i;
const POST_RE = /^post$/i;
const isUrlEncoded = (req) => {
  var type = req.headers?.['content-type'];
  if (!type || !POST_RE.test(req.method)) {
    return;
  }
  return URLENCODED_RE.test(type);
};

const formatData = (session, utils, isRes) => {
  let data = session[isRes ? 'res' : 'req'];
  let body = data.body || '';
  let raw;
  let json;

  const getBody = () => {
    if (body == null) {
      raw = raw || (isRes ? utils.getRawRes(data) : utils.getRawReq(data));
      body = raw ? raw.substring(raw.indexOf('\r\n\r\n') + 4) : '';
    }
    return body;
  };

  Object.defineProperties(data, {
    body: {
      configurable: true,
      get: getBody,
    },
    json: {
      configurable: true,
      get() {
        if (json) {
          return json;
        }
        getBody();
        if (body) {
          if (!isRes && isUrlEncoded(data)) {
            json = qs.parse(body);
          } else {
            try {
              json = JSON.parse(body);
            } catch (e) {}
          }
        }
        json = json || {};
        return json;
      }
    },
    raw: {
      configurable: true,
      get() {
        if (raw == null) {
          raw = isRes ? utils.getRawRes(data) : utils.getRawReq(data);
        }
        return raw;
      }
    },
  });
};

const formatSession = (session, utils) => {
  let timings;
  let rules;
  Object.defineProperties(session, {
    timings: {
      configurable: true,
      get() {
        timings = timings || utils.getTimings(session);
        return timings;
      }
    },
    rawRules: {
      configurable: true,
      get() {
        rules = rules || utils.getRules(session);
        return rules.join('\n');
      }
    },
  });
  formatData(session, utils);
  formatData(session, utils, false);
  return session;
};

/**
 * @async 获取抓包数据(支持自动分页拉取)
 * @function getSessions
 * @description
 * 用于按条件检索 HTTP/WebSocket 等协议的抓包记录。支持基于游标、时间、资源类型、URL 关键词、请求方法、状态码以及请求/响应头的组合筛选。
 *
 * **过滤逻辑**:
 * - 所有非空条件均以 **AND** 关系叠加(即需同时满足);
 * - 对于 `method` 和 `statusCode`,传入多个值时采用 **OR** 关系(匹配任一即可);
 * - `latest` 优先级最高,一旦为 `true`,count 最多只返回 120 条最新记录;
 * - 若同时提供 `startId` 与 `startTime`,系统将在**首轮请求**中以较新者作为起始点,后续分页仅使用 `startId`(`startTime` 会被自动清除);
 * - `reqHeader` 和 `resHeader` 为对象类型,支持按请求头/响应头的字段名和值进行子串匹配。
 *
 * **分页机制**:
 * - 每次最多从 Whistle 服务拉取 120 条记录(底层 API 限制),函数会**自动循环分页**,直到满足 `count` 指定的总数或到达最新数据末尾。
 * - 分页游标自动使用上一页最后一条记录的 `id` 作为下一页的 `startId`,并清除 `startTime`,确保后续分页仅基于游标。
 * - 若 `latest=true`,则只请求一次,返回最新的若干条(最多 120 条),忽略 `count` 和任何游标/时间参数。
 * - **重要限制**:由于 Whistle API 的 `startId` 是“**大于**”语义(即只能向更晚时间滚动),**若未提供 `startId` 或 `startTime`**,函数将从最新数据开始拉取,**只能获取第一页(最多 120 条)**,后续请求尝试获取更新的数据通常为空。如需获取大量历史数据,请先通过其他方式获取一个较早的 `startId` 作为起点。
 *
 * @param {Object} options - 配置对象
 * @param {number} [options.count=3600] - 期望获取的最大记录数。实际返回数量 ≤ 此值,且受每页 120 条限制及数据实际数量影响。若 `latest=true`,此参数无效。
 * @param {boolean} [options.latest=false] - 是否仅获取最新的记录。
 *                                           - `true`:只请求一次,返回最新的若干条(最多 120 条);
 *                                           - `false`:配合 `startId` 或 `startTime` 进行增量拉取,并自动分页直到凑够 `count` 条或到达数据末尾。
 * @param {string|number} [options.startId] - 游标 ID(通常为记录的自增主键或唯一序号,格式如 `"1784905385300-003"`)。
 *                                            获取记录 ID **大于**此值的增量数据(即更新的数据)。
 *                                            若同时提供 `startTime`,则首轮请求取两者中更靠近最新数据的一个作为起始点,后续分页仅使用 `startId`。
 * @param {number} [options.startTime] - 起始时间戳(Unix 毫秒级)。
 *                                       获取记录生成时间 **大于**此值的增量数据。
 *                                       仅在首轮请求中生效(若同时提供 `startId` 则取较新者),后续分页会被清除。
 * @param {string} [options.type] - 资源类型过滤,同抓包界面 Network 底部过滤栏选项(不区分大小写)。
 *                                  支持以下枚举值(传入其它字符串表示不过滤):
 *                                  - `JSON`:JSON 请求/响应
 *                                  - `HTML`:HTML 文档
 *                                  - `CSS`:样式表
 *                                  - `JS`:JavaScript 脚本
 *                                  - `Font`:字体文件
 *                                  - `Img`:图片资源
 *                                  - `Media`:音视频媒体
 *                                  - `WS`:WebSocket 帧数据
 *                                  - `Tunnel`:隧道连接(如 WebSocket 升级)
 *                                  - `Wasm`:WebAssembly 模块
 *                                  - `Mock`:Mock 数据
 *                                  - `Rules`:匹配规则的请求
 *                                  - `Import`:导入的数据
 *                                  - `Composer`:通过 Composer 发送的请求
 *                                  - `Error`:错误请求(状态码 >= 400、captureError、中断的请求等)
 *                                  - `captureError`:解析 HTTPS 失败的 Tunnel 请求
 * @param {string} [options.subUrl] - URL 中包含该字符串的请求(不区分大小写)。
 *                                    例如 `"/api/user"` 将匹配所有包含该路径的 URL。
 * @param {string|string[]} [options.method] - HTTP 请求方法,支持多个(数组或逗号分隔字符串,如 `"GET,POST"`,不区分大小写)。
 *                                             匹配任一方法即满足条件。
 * @param {number|number[]} [options.statusCode] - HTTP 响应状态码,支持多个(数组或逗号分隔字符串,如 `"200,404"`)。
 *                                                 匹配任一状态码即满足条件。
 * @param {Object} [options.reqHeader] - 请求头过滤条件。
 * @param {string} options.reqHeader.name - 请求头字段名称(如 `"X-Request-Id"`,不区分大小写)。
 * @param {string} [options.reqHeader.subValue] - 可选,请求头字段值需要包含的子字符串(不区分大小写)。
 *                                                若不填,则表示仅要求存在该请求头字段(无论值为何)。
 * @param {Object} [options.resHeader] - 响应头过滤条件。
 * @param {string} options.resHeader.name - 响应头字段名称(如 `"Content-Type"`,不区分大小写)。
 * @param {string} [options.resHeader.subValue] - 可选,响应头字段值需要包含的子字符串(不区分大小写)。
 *                                                若不填,则表示仅要求存在该响应头字段(无论值为何)。
 *
 * @returns {Promise<Array<Object>>} 返回符合条件的抓包记录数组(经过 `formatSession` 格式化处理)。
 *                                    每条记录包含以下主要字段(可能因 `formatSession` 处理而略有增减):
 *                                    - `id` (string): 记录唯一标识,格式为 `"时间戳-序号"`(如 `"1785144349945-573"`)
 *                                    - `startTime` (number): 请求开始时间戳(Unix 毫秒级)
 *                                    - `url` (string): 完整请求 URL
 *                                    - `req` (Object): 请求详情
 *                                      - `method` (string): 请求方法
 *                                      - `ip` (string): 客户端 IP
 *                                      - `port` (string): 客户端端口
 *                                      - `headers` (Object): 请求头键值对
 *                                      - `json` (Object): 解析后的 JSON 请求体(若存在)
 *                                      - `body` (string): 原始请求体(可能为空)
 *                                      - `raw` (string): 原始请求数据
 *                                    - `res` (Object): 响应详情
 *                                      - `ip` (string): 服务器 IP
 *                                      - `port` (string): 服务器端口
 *                                      - `statusCode` (number): HTTP 状态码
 *                                      - `headers` (Object): 响应头键值对
 *                                      - `json` (Object): 解析后的 JSON 响应体(若存在)
 *                                      - `body` (string): 原始响应体(可能为空)
 *                                      - `raw` (string): 原始响应数据
 *                                    - `timings` (Object): 请求各阶段耗时(单位:毫秒)
 *                                      - `start` (number): 请求开始的时间戳(相对于某个基准点)
 *                                      - `ttfb` (number | null): 首字节时间
 *                                      - `dns` (number | null): DNS 解析耗时
 *                                      - `connect` (number | null): TCP 连接耗时(从 DNS 结束到连接建立)
 *                                      - `request` (number | null): 请求发送耗时(从 DNS 结束到请求结束)
 *                                      - `response` (number | null): 响应接收耗时(从请求结束到接收到响应头)
 *                                      - `download` (number | null): 下载耗时(从接收响应头到接收完响应体)
 *                                      - `total` (number | null): 总耗时
 *                                    - `rules` (Object): 匹配的规则对象
 *                                    - `rawRules` (string): 原始规则字符串
 *                                    - `version` (string): Whistle 版本号
 *                                    - `nodeVersion` (string): Node.js 版本号
 *                                    - `dnsTime` (number): DNS 完成时间戳
 *                                    - `requestTime` (number): 请求发送完成时间戳
 *                                    - `connectTime` (number): 连接建立完成时间戳
 *                                    - `responseTime` (number): 响应头接收完成时间戳
 *                                    - `endTime` (number): 请求结束时间戳
 *
 * @throws {Error} Whistle API 加载失败(如 `w2` 命令不可用、模块加载错误,需确保 whistle 已全局安装)
 * @throws {Error} Whistle 版本低于 2.10.7
 * @throws {Error} Whistle 服务未启动或 `api.network.getSessions()` 调用失败(如网络异常、参数错误)
 * @throws {Error} 底层 API 调用可能抛出其他运行时错误(如超时、数据格式异常)
 *
 * @example
 * // 使用游标增量拉取(最多 100 条)
 * const sessions = await getSessions({ startId: '1784905385300-003', count: 100 });
 *
 * @example
 * // 获取最新的 JSON 类型记录(最多 120 条)
 * const sessions = await getSessions({ latest: true, type: 'JSON' });
 *
 * @example
 * // 组合过滤:查询状态码为 200 或 304 的 GET 请求,且 URL 包含 "/v1",同时要求存在特定请求头
 * const sessions = await getSessions({
 *   subUrl: '/v1',
 *   method: 'GET',
 *   statusCode: [200, 304],
 *   reqHeader: { name: 'X-Token', subValue: 'abc' },
 *   count: 50
 * });
 */
const getSessions = async (options) => {
  options = { ...options };
  let count = +options.count || 3600;
  const result = [];
  const api = await loadApi();
  while (count > 0) {
    const sessions = await api.network.getSessions(options);
    const len = sessions.length;
    for (let i = 0; i < len; i++) {
      result.push(formatSession(sessions[i], api.utils));
    }
    if (len < 120 || options.latest) {
      break;
    }
    options.startId = sessions[len - 1]?.id;
    options.startTime = undefined;
    count = count - len;
  }
  return result;
};


const formatFrame = (frame, utils) => {
  let body;
  let json;

  const getBody = () => {
    if (body == null) {
      body = utils.getText(frame);
    }
    return body;
  };
  Object.defineProperties(frame, {
    body: {
      configurable: true,
      get: getBody,
    },
    json: {
      configurable: true,
      get() {
        getBody();
        if (body) {
          try {
            json = JSON.parse(getBody());
          } catch (e) {}
        }
        json = json || {};
        return json;
      },
    }
  });
  return frame;
}

/**
 * @async 获取 WebSocket/Socket 的帧数据(支持自动分页拉取)
 * @function getFrames
 * @description
 * 按条件检索指定连接(通过 `reqId` 关联)的 WebSocket/Socket 通信帧,支持基于游标、时间、来源方向的组合筛选。
 *
 * **过滤逻辑**:
 * - `from` 参数用于过滤帧的发送方向:`client`(客户端→服务端)、`server`(服务端→客户端),不传则返回全部方向;
 * - `latest` 优先级最高,一旦为 `true`,count 最大为 120;
 * - 若同时提供 `startId` 与 `startTime`,系统将在**首轮请求**中以较新者作为起始点,后续分页仅使用 `startId`(`startTime` 会被自动清除);
 *
 * **分页机制**:
 * - 每次最多从 Whistle 服务拉取 120 条帧记录(底层 API 限制),函数会**自动循环分页**,直到满足 `count` 指定的总数或到达最新帧末尾。
 * - 分页游标自动使用上一页最后一条帧的 `id` 作为下一页的 `startId`,并清除 `startTime`,确保后续分页仅基于游标。
 * - 若 `latest=true`,则只请求一次,返回最新的若干条(最多 120 条),忽略 `count`。
 * - **重要限制**:由于 Whistle API 的 `startId` 是“**大于**”语义(即只能向更晚时间滚动),**若未提供 `startId` 或 `startTime`**,函数将从最新帧开始拉取,**只能获取第一页(最多 120 条)**,后续请求尝试获取更新的帧通常为空。如需获取大量历史帧,请先通过其他方式获取一个较早的 `startId` 作为起点。
 *
 * @param {Object} options - 配置对象
 * @param {string} options.reqId - 会话 ID,对应抓包记录的 `id` 字段,用于关联帧所属的连接
 * @param {number} [options.count=3600] - 期望获取的最大帧数。实际返回数量 ≤ 此值,且受每页 120 条限制及实际帧数量影响。若 `latest=true`,此参数无效。
 * @param {boolean} [options.latest=false] - 是否仅获取最新的帧。
 *                                           - `true`:只请求一次,返回最新的若干条(最多 120 条);
 *                                           - `false`:配合 `startId` 或 `startTime` 进行增量拉取,并自动分页直到凑够 `count` 条或到达数据末尾。
 * @param {string|number} [options.startId] - 游标 ID(帧的唯一序号),获取帧 ID **大于**此值的增量数据(即更新的帧)。
 *                                            若同时提供 `startTime`,则首轮请求取两者中更靠近最新数据的一个作为起始点,后续分页仅使用 `startId`。
 * @param {number} [options.startTime] - 起始时间戳(Unix 毫秒级),获取帧生成时间 **大于**此值的增量数据。
 *                                       仅在首轮请求中生效(若同时提供 `startId` 则取较新者),后续分页会被清除。
 * @param {string} [options.from] - 帧的来源方向过滤(不区分大小写):
 *                                   - `'client'`:仅获取客户端→服务端的帧
 *                                   - `'server'`:仅获取服务端→客户端的帧
 *                                   - 不传或传入其他值:获取该连接的所有帧
 *
 * @returns {Promise<Array<Object>>} 返回符合条件的帧数据数组(经过 `formatFrame` 格式化处理)。
 *                                    每条帧包含以下主要字段:
 *                                    - `reqId` (string): 所属会话 ID,对应抓包记录的 `id`
 *                                    - `frameId` (string): 帧数据的唯一标识 ID(可用于游标)
 *                                    - `mask` (boolean): 是否为掩码帧(WebSocket 掩码)
 *                                    - `compressed` (boolean): 帧数据是否经过压缩
 *                                    - `length` (number): 帧数据的原始长度(字节数)
 *                                    - `opcode` (number): 帧操作码,`1` 表示文本帧,`2` 表示二进制帧
 *                                    - `base64` (string): Base64 编码的帧数据(用于二进制或大文本)
 *                                    - `body` (string): 解码后的帧内容(文本或可读字符串)
 *                                    - `json` (Object): 若帧数据为 JSON 格式,则解析为对象,否则为空对象 `{}`
 *
 * @throws {Error} 依赖 `loadApi` 可能抛出的错误(如 Whistle 未安装、版本过低、服务未启动)
 * @throws {Error} 底层 `api.network.getFrames()` 调用失败(如 `reqId` 不存在、参数错误、网络异常)
 *
 * @example
 * // 使用游标增量拉取,获取客户端发出的前 10 条帧
 * const frames = await getFrames({
 *   reqId: '1784885309943-086',
 *   startId: '1784903620142-000',
 *   startTime: 1784903620156,
 *   count: 10,
 *   from: 'client'
 * });
 *
 * @example
 * // 获取最新的帧(最多 120 条)
 * const frames = await getFrames({
 *   reqId: '1784885309943-086',
 *   latest: true
 * });
 *
 * @example
 * // 从指定游标开始拉取所有后续帧(直到最新)
 * const frames = await getFrames({
 *   reqId: '1784885309943-086',
 *   startId: '1784903620142-000',
 *   count: 3600
 * });
 */
const getFrames = async (options) => {
  options = { ...options };
  let count = +options.count || 3600;
  const result = [];
  const api = await loadApi();
  while (count > 0) {
    const frames = await api.network.getFrames(options);
    const len = frames.length;
    for (let i = 0; i < len; i++) {
      result.push(formatFrame(frames[i], api.utils));
    }
    if (len < 120 || options.latest) {
      break;
    }
    options.startId = frames[len - 1]?.frameId;
    options.startTime = undefined;
    count = count - len;
  }
  return result;
};