OneKuma

OneKuma's Blog

One Lonely Kuma.
github
bilibili
twitter

构建一个易用的 aria2 客户端包和 CLI 应用

最近有空把之前弄了一半的 aria2 的包继续搞了搞,写一点东西,记录过程中一些有趣的点。

naria2 是一个调用 aria2 RPC 接口、封装抽象后的 aria2 下载器 JavaScript / TypeScript 客户端库,同时也是一个在 aria2c 基础上提供一些额外功能的 CLI 工具应用

naria2c version

naria2 ui

跨平台的统一安装方式#

在使用 aria2 之前,我们必须先下载 aria2。这似乎是一句废话,毕竟 aria2 一般不是系统自带的东西,但作为一个 C++ 写的原生应用,显然它的下载安装不一定 trivial。

于是,我们就有一些可能的方案。比如,我作为一个客户端的包为什么要帮用户下载,你下好了,加到 PATH 里再来用吧。OK,没问题,文档里多写两句就行了,但是总归不是很优雅。

所以,我希望,使用这个库的用户不需要太多的感知到一些细节:我们会在后台启动了一个 aria2 RPC 接口服务的进程。用户只需要知道

  1. 调用几个简单的异步函数,完成客户端的初始化;
  2. 通过封装抽象好的 API 完成 Torrent 的各种下载、监控任务;
  3. 所有逻辑处理完成后,销毁客户端。

所以,我们还是希望为不同平台的用户,提供一个方便的方式下载 aria2。

拉取并运行一个远程的安装脚本#

目前相当多的跨平台应用选择这种安装方式,它们会搞个 Web 服务用于分发一段安装脚本。以 Rust 为例,复制图上这条命令,一路回车就能装好一个 Rust。

image

但是,从这张图上就能看出,只有 Windows 特殊,需要下一个安装包来安装。因为,这些 *nix 系统能跑的脚本,在 Windows 的 PowerShell 和 CMD 上跑起来并不容易,比如贴心的微软就帮你把 Invoke-WebRequest (iwr) 起了个 "curl" 的别名,但是用法和 curl 完全不同。

第二,维护一个分发脚本的服务简单也不简单。简单是因为,随便搞个部署静态页面的平台或者 Serverless 函数平台,分分钟就能把脚本搞上去。但是,你是不是需要买个域名,搞 SSL 证书,隔几年续费域名,国内还要考虑墙内等等,即使感谢 Cloudflare 把一些脏活累活都给你做好了,但是还是有点麻烦了。当然也有简便的方法,感谢 egoist 几年的一个项目 bina,大概就是根据你项目的 GitHub Releasse 内的标有平台的文件名,自动生成一段用于下载安装 CLI 应用到 PATH 的脚本

image

发到一堆包管理器上#

Rust 应用经典地域绘图。你说得对,但是 Rust 是由 Mozilla 自主研发的一款全新内存安全编程语言。编译将发生在一个被称作「卡尔构」的构建系统,在这里,被引用的指针将被授予「生命周期」,导引安全之力。你将扮演一位名为「开发者」的神秘角色在编程的搏斗中邂逅骨骼惊奇的报错,绕开它们通过编译同时,逐步发掘「Rust」的真相。

image

实际上,我认为,使用包管理器和抓取分发的安装脚本来运行,两者并无过多的区别。只是包管理器,第一,将 "抓取安装的东西" 和 "运行本地安装脚本" 的逻辑给做成了内部的实现,第二,帮你维护了一个包的列表和包的资源,节省了自己精力,也不容易跑路。当然包管理器能做到更多事情,比如统一管理版本、依赖等等。

除此以外,还有 nix(不太了解),tea (结合了 Web3?)等等包管理方案,本文目的也不是介绍这些东西,这两节只是顺便随便聊聊一些方案罢了。

利用 package.json 的 optionalDependencies#

得益于 Node 的一些跨平台特性,于是我们考虑发到 npm 上。如果你对目前的前端工具链有所了解的话,例如 esbuildswc 这类使用原生语言开发的应用可以通过 npm 进行分发。

esbuild 为例,好奇的你点开它的 npm 主页:

image

你会发现,它依赖了一堆东西,包括各种各样的平台。如果再点开它的 package.json,可以发现这些实际上都是 optionalDependencies

{
  "name": "esbuild",
  "version": "0.19.5",
  "description": "An extremely fast JavaScript and CSS bundler and minifier.",
  "repository": "https://github.com/evanw/esbuild",
  "scripts": {
    "postinstall": "node install.js"
  },
  "main": "lib/main.js",
  "types": "lib/main.d.ts",
  "engines": {
    "node": ">=12"
  },
  "bin": {
    "esbuild": "bin/esbuild"
  },
  "optionalDependencies": {
    "@esbuild/android-arm": "0.19.5",
    "@esbuild/android-arm64": "0.19.5",
    "@esbuild/android-x64": "0.19.5",
    "@esbuild/darwin-arm64": "0.19.5",
    "@esbuild/darwin-x64": "0.19.5",
    "@esbuild/freebsd-arm64": "0.19.5",
    "@esbuild/freebsd-x64": "0.19.5",
    "@esbuild/linux-arm": "0.19.5",
    "@esbuild/linux-arm64": "0.19.5",
    "@esbuild/linux-ia32": "0.19.5",
    "@esbuild/linux-loong64": "0.19.5",
    "@esbuild/linux-mips64el": "0.19.5",
    "@esbuild/linux-ppc64": "0.19.5",
    "@esbuild/linux-riscv64": "0.19.5",
    "@esbuild/linux-s390x": "0.19.5",
    "@esbuild/linux-x64": "0.19.5",
    "@esbuild/netbsd-x64": "0.19.5",
    "@esbuild/openbsd-x64": "0.19.5",
    "@esbuild/sunos-x64": "0.19.5",
    "@esbuild/win32-arm64": "0.19.5",
    "@esbuild/win32-ia32": "0.19.5",
    "@esbuild/win32-x64": "0.19.5"
  },
  "license": "MIT"
}

然后,我们再随便点开一个包,可以发现每个 @esbuild/* 里其实只有一个对应平台的 esbuild 二进制文件和一个 package.json。以 @esbuild/win32-x64 为例,可以看到它的 package.json 通过 oscpu 两个字段指定了这个包支持的平台

{
  "name": "@esbuild/win32-x64",
  "version": "0.19.5",
  "description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
  "repository": "https://github.com/evanw/esbuild",
  "license": "MIT",
  "preferUnplugged": true,
  "engines": {
    "node": ">=12"
  },
  "os": [
    "win32"
  ],
  "cpu": [
    "x64"
  ]
}

安装 optionalDependencies 的逻辑大概是,会拿取当前 process.platformprocess.arch 来判断每个包是否能够匹配它的平台,npm 只会下载对应平台的包(可以传一些参数让它强制下载别的,你可以自己试试,看看自己项目里的 swcesbuild 到底用了哪个平台的版本)。

除此以外,可以看到 esbuild 的包里的 scripts 内有 postinstall 一项,它除了作为脚本可以 npm run 以外,还会 自动在 npm install 后执行(其它包管理器类似),完成其它一些初始化工作,比如它就运行一个 postinstall.js 脚本。

@naria2/node#

总而言之,使用类似的配置方式,从 agalwood/Motrix 偷了几个构建好的 aria2 二进制,发到了 npm 上,具体参考:packages/binary

感觉其它一些东西也可以用类似的方法发到 npm 上,这样以后只需要先用 Volta / fnm 搞一个 Node 下来,然后用 npm 来下载其它常用的工具。

然后,下载好了 aria2 的二进制,你还需要写一些代码,根据当前 process.platformprocess.arch 选择对应的二进制包:

function getPackage() {
  const { platform, arch } = process;

  switch (platform) {
    case 'win32':
      if (['x64', 'ia32'].includes(arch)) {
        return `@naria2/win32-${arch}`;
      }
    case 'darwin':
      if (['x64', 'arm64'].includes(arch)) {
        return `@naria2/darwin-${arch}`;
      }
    case 'linux':
      if (['x64', 'arm64'].includes(arch)) {
        return `@naria2/linux-${arch}`;
      }
  }

  throw new Error('naria2 does not provide aria2 binary of your platform');
}

export const BINARY = getPackage();

export function getBinary() {
  const pkg = require.resolve(BINARY + '/package.json');
  const { platform } = process;
  // Windows should end with .exe
  const binary = path.join(path.dirname(pkg), platform === 'win32' ? 'aria2c.exe' : 'aria2c');
  return binary;
}

最后,你只需要调用 getBinary() 就能找到当前平台下的 aria2 二进制的绝对路径。

从 Node.js 的代码中启动#

经过上面的努力,我们已经把 aria2 的二进制,用类似 esbuildswc 的方式下到了 node_modules 里。当然这还不够,继续向 "调用几个简单的异步函数,完成客户端的初始化" 努力。

maria2 是使用的一个基础的 aria2 RPC 接口客户端,它的抽象方式给了我启发。

  • 使用 WebSocket 连接:
import { open, aria2 } from 'maria2'

const conn = await open(
  new WebSocket('ws://localhost:6800/jsonrpc')

  // import { createWebSocket } from 'maria2/transport'
  // createWebSocket('ws://localhost:6800/jsonrpc')
)

const version = await aria2.getVersion(conn)
  • 使用 HTTP 连接
import { open, aria2 } from 'maria2'
import { createHTTP } from 'maria2/transport'

const conn = await open(
  createHTTP('http://localhost:6800/jsonrpc')
)

const version = await aria2.getVersion(conn)

我们可以类似地把从本地启动的一个 aria2 进程,连带着和它的 WebSocket 连接封装成一个东西。将原本的建立 WebSocket 连接,变成 先从 node_modules 里启动 aria2 进程,然后创建 WebSocket 连接

export async function createChildProcess(): Promise<ChildProcessSocket> {
  const child = spawn(, ['--enable-rpc']);
  await new Promise((res, rej) => {
    let spawn = false;

    if (child.stdout) {
      child.stdout.once('data', () => {
        spawn = true;
        res(undefined);
      });
    } else {
      child.once('spawn', () => {
        spawn = true;
        res(undefined);
      });
    }

    child.once('error', (e) => {
      if (!spawn) {
        rej(e);
      }
    });
  });

  return new WebSocket(`ws://127.0.0.1:6800/jsonrpc`);
}

以上示例代码,省略了亿点细节,但总之就是这么个意思。接下来就是,README.md 中描述的效果了:

npm i naria2 @naria2/node

使用时,只需要 await createClient(createChildProcess()) 一行就能完成 aria2 进程的创建和 WebSocket 连接的建立,同样 client.close() 也包含了 aria2 进程关闭的逻辑。

import { createClient } from 'naria2'
import { createChildProcess } from '@naria2/node'

// Initialize a client
const client = await createClient(createChildProcess())

// Start downloading a magnet
const torrent = await client.downloadUri('...')

// Watch torrent progress
await torrent.watchFollowedBy((torrent) => {
  console.log(`Downloading ${torrent.name}`)
})

// Shutdown client
await client.shutdown()

当然可能还有一些无法忽略的细节,比如,此处这样启动的 aria2 进程可能会在脚本异常退出后,仍然保持存活,可能需要参考 一个 Node.js 命令行程序的『死亡』 完成此类的边界情况的处理。

作为 CLI 应用启动#

我们既然已经有了当前平台的 aria2 二进制,而且 aria2 本来就是一个 CLI 下载工具,我们完全可以顺便用上面的方式套一层,化身一个跨平台的 CLI 下载工具。

所以,在 Windows / Mac OS / Linux 上,只要你有 Node 的环境,就能直接和其它包一样 npm i -g

npm i -g naria2c

最初,它干的事情非常简单,拿到真实的 aria2 路径,然后用 execa 运行一下,起一个好听的名字就叫 naria2c 了。

#!/usr/bin/env node

import { execa } from 'execa'
import { getBinary } from '@naria2/node'

const binary = getBinary()

const childProcess = await execa(binary, process.argv.slice(2))

然后,我们给它再加入亿点细节。

转换 naria2c 的输出#

因为做了一层的包裹,现在运行的表面上已经不是 aria2c 了,但是如果你使用 naria2c --help 仍然会看到,它提示你使用 aria2c 来运行程序,这无疑会让用户产生困惑。

于是,我们需要对 aria2c 的输出流和错误流做一些转换,将 aria2c 转换为 naria2c

import { Transform } from 'stream'

childProcess.stdout.pipe(transformOutput()).pipe(process.stdout)
childProcess.stderr.pipe(transformOutput()).pipe(process.stderr)

function transformOutput() {
  return new Transform({
    transform(chunk, encoding, callback) {
      let text = chunk.toString()
      text = text.replace(/aria2c/g, 'naria2c')
      callback(null, text)
    }
  });
}

但是,这可能会引入某些错误的替换,类似 Production Replacement | Vite

于是,naria2c 的实际的选择:

  1. 我们会先看一下是否在运行是 -h, --help 或者 -v, --version,可以只过滤帮助信息和版本信息命令输出内的 aria2c
  2. 对于标准错误流,它可能会携带帮助信息,使用上面的转换流(但是替换字符串的格式更加具体)。

image

image

看起来就像真的是自己写的一样(

透传 aria2 进程的终止信号#

根据 一个 Node.js 命令行程序的『死亡』,透传了一些终止的信号给 aria2 进程。

import { onDeath } from '@breadc/death';

const cancelDeath = onDeath(async (signal) => {
  const killChildProcess = () => {
    childProcess.kill(signal);
    return new Promise((res) => {
      if (childProcess.exitCode !== null || childProcess.killed) {
        res();
      } else {
        childProcess.once('exit', () => {
          res();
        });
      }
    });
  };

  await Promise.race([
    killChildProcess(),
    sleep(5000);
  ]);
});

但是由于 Windows 平台和其它平台的信号机制不太一样,这部分似乎不知道怎么模拟原本 aria2c 的终止行为。期望是,按 Ctrl-C 后,aria2 退出并输出一些总结信息,或者触发一次 aria2 的平缓退出,再按一次强制退出。

但是,反正,又不是不能用(

禁止 aria2 自动使用代理环境变量#

由于机场有 BitTorrent 相关的审计规则,aria2c 会自动根据环境变量里的 http_proxy 等设置代理,很容易一不小心,忘记关代理导致触发机场的审计。

因为,我们会手动开一个 aria2c 进程,于是,我们可以在开之前做一些预处理,给它把代理相关的环境变量清掉。

首先,添加一个新的选项 -I, --ignore-proxy 用于清除代理相关的环境变量;然后,在创建进程时,传一个过滤后的 process.env 给它。

// 省略命令行参数的解析

const env = options.ignoreProxy
  ? {
      ...process.env,
      HTTP_PROXY: undefined,
      HTTPS_PROXY: undefined,
      ALL_PROXY: undefined,
      http_proxy: undefined,
      https_proxy: undefined,
      all_proxy: undefined,
      no_proxy: undefined
    }
  : process.env;

添加启动 Web UI 的命令行参数#

如果你只是想像 curl 等等 CLI 工具一样,只是下载一个 Torrent,那么现在的方式已经完全够用了,但是如果要操作多个 Torrent、监控它们的实时状态等等,TUI 的形式显然并不是很方便,我们还是希望能有一个 GUI 来操控 aria2

那么,既然我们本身就在做一个 aria2 RPC 客户端的库,何不直接用它来构建另一个 aria2 的 Web 应用呢?

为此,我们给 naria2c 添加 Web UI 的相关参数 --ui,用于启动 aria2c 进程的同时,启动一个 Web UI。

naria2c --ui

web-ui

目前 Web UI 只实现了一些简单的可视化和控制。

naria2c --ui 一个命令就能完成 aria2c 和 Web UI 的启动。但是,这底下还涉及一些细节:

  • --ui 参数不是 aria2c 支持的,需要在启动脚本中过滤提取出来;
  • 打开 Web UI 一定伴随着启动 RPC 服务,因此,如果发现 --ui 选项开启,那么就需要同时设置 --enable-rpc
  • Web UI 需要知道 RPC 服务的端口号和认证密钥,显然这个信息不需要重复指定,只需要手动解析 --rpc-secret 选项即可。
// aria2c 的参数
const args: string[] = []

// 缺失的参数
const missing = {
  enableRpc: true
}

// Web UI 相关的参数
const webui = {
  enable: false,
  secret: undefined
}

for (const arg of process.argv.slice(2)) {
  // 省略解析命令行参数
}

// 启动了 Web UI, 但是未启动 RPC, 补充参数
if (webui.enable && missing.enableRpc) {
  args.push('--enable-rpc')
}

于是,我们从命令行参数中获取到了想要的信息,接下来就可以启动嵌入的 Web UI 了。

嵌入的 Web UI#

紧跟着上一小节,为什么我要嵌入一个 Web UI 到 naria2c 里?

回想最开始的时候:

  • naria2 实际上只是一个客户端库,能跑在浏览器,Node 等等环境里;
  • @naria2/node 为它提供了 Node 相关的本地运行支持,例如支持本地直接打开 aria2 进程;
  • naria2caria2c 的跨平台 wrapper,只是一个附加产物。

那么,我们似乎应该更优先考虑,能给客户端多提供一些什么能力:启动 Web UI 完全可以归到 @naria2/node 中,作为一个包的能力给它的使用者

除此以外,不是使用 electron 之类的技术,做一个桌面端的 GUI 应用(例如 agalwood/Motrix)。第二,做桌面端应用无法满足上面作为包的需要,因为这需要添加很多额外的打包分发成本,就像下载 aria2c 的二进制一样。

实际上,这也是我的真实需求:AnimeSpace,另一个自动的新番下载、整理解决方案。AnimeSpace 是一个命令行程序,它会自动根据订阅情况,下载动画资源的 Torrent。而目前的版本,它下载时只显示一个下载进度条的 TUI,不是非常直观。引入 @naria2/node 的这个能力,可以为它提供更丰富的下载进度 GUI

启动 Web UI#

那么如何嵌入 Web UI 到 @naria2/node

实际上只需要将前端应用打包构建的产物,复制到这个包下的某个目录内(并对 package.json 中的 files 项进行相关的配置),然后在运行时找到前端应用构建产物的目录,在其上创建一个静态文件的 HTTP 服务器即可。

@naria2/node 对应的包结构如下图所示:

@naria2/node
 ├─ client/          # 前端应用的构建产物
 │   ├─ assets/**
 │   └─ index.html
 ├─ dist/**          # TypeScript 源码转译的产物
 ├─ ...
 └─ package.json

潜在的问题

  • 路径依赖于 TypeScript 转译构建产物的路径;
  • 无法支持 single-executable-applications 或者内联依赖的打包方式,因为它依赖这些非代码的静态产物在文件系统中相对位置。

但是问题不大。

于是,我们可以通过 fileURLToPath(new URL('../client', import.meta.url)) 的方式来找到 Web UI 产物的路径。使用 serve-static 来启动一个本地文件的 HTTP 服务器。

最终,用户就能直接调用 await launchWebUI({ ... }) 来启动一个 Web UI。

export async function launchWebUI(options: WebUIOptions) {
  const serveStatic = (await import('serve-static')).default;
  const finalhandler = (await import('finalhandler')).default;
  const http = await import('http');

  const port = options.port ?? 6801;
  const clientDir = fileURLToPath(new URL('../client', import.meta.url));
  const serve = serveStatic(clientDir, { index: ['index.html'] });
  const server = http.createServer(async (req, res) => {
    serve(req, res, finalhandler(req, res));
  });

  server.listen(port);

  return server;
}

export interface WebUIOptions {
  port?: number;

  rpc: {
    port: number;

    secret: string | undefined;
  };
}

vite-plugin-naria2#

之前从来没有写过 React,在网上云了很多,这次决定拿这个东西试一下怎么写。这里我们不需要 SSR,所以直接 Vite 就行了,外加了一堆 React 生态的东西,react-routerTanStack QueryZustandtailwindcssshadcn,以及一堆 shadcn 依赖的东西。一开始看起来挺复杂的,尤其是 shadcn 弹出的一坨组件代码,看的脑阔疼,但似乎写起来还是很方便的,而且默认的功能很强大。

开始开发这个应用的第一个问题是,我们需要起一个 aria2 的服务给前端使用,于是参考之前写的插件 vite-plugin-cloudflare-functions 的思路。

直接在 Vite 插件里启动一个 aria2,然后通过虚拟模块传递连接信息给前端使用。因此就不用另外启动服务,另外配置了。

export default function Naria2(): Plugin[] {
  const childProcessRuntime = {
    process: undefined as ChildProcessSocket | undefined,
    url: undefined as string | undefined,
    secret: undefined as string | undefined
  };

  return [
    {
      name: 'vite-plugin-naria2:runtime',
      apply: 'serve',
      async configureServer(server) {
        if (!childProcessRuntime.url) {
          // 在 Vite dev 服务器里启动一个 aria2
          const childProcess = await createChildProcess();
          childProcessRuntime.process = childProcess;
          childProcessRuntime.url = `ws://127.0.0.1:${childProcess.getOptions().listenPort}/jsonrpc`;
          childProcessRuntime.secret = childProcess.getOptions().secret;
        }
      },
      closeBundle() {
        childProcessRuntime.process?.close?.();
        childProcessRuntime.url = undefined;
        childProcessRuntime.secret = undefined;
      }
    },
    {
      name: 'vite-plugin-naria2:build',
      resolveId(id) {
        if (id === '~naria2/jsonrpc') {
          return '\0' + id;
        }
      },
      load(id) {
        if (id === '\0~naria2/jsonrpc') {
          // 作为虚拟模块传递信息给前端
          const socketCode = childProcessRuntime.url
            ? `new WebSocket(${JSON.stringify(childProcessRuntime.url)})`
            : 'undefined';
          const clientCode = `socket ? await createClient(socket, { ${
            childProcessRuntime.secret
              ? 'secret: ' + JSON.stringify(childProcessRuntime.secret)
              : ''
          } }) : undefined`;

          return [
            `import { createClient } from 'naria2';`,
            `export const socket = ${socketCode};`,
            `export const client = ${clientCode};`
          ].join('\n');
        }
      }
    }
  ];
}

打开本地资源管理器#

我们还希望给这个 Web UI 加一些常用的功能,比如打开本地资源管理器

但是,由于浏览器的安全限制,实际上这个应该不太方便直接做到。

但是,我们实际上启动了一个后端服务,直接用这个服务来打开不就行了?

export async function launchWebUI(options: WebUIOptions) {
  // ...
  const handler = await createWebUIHandler(options);
  const server = http.createServer(async (req, res) => {
    if (await handler(req, res)) {
      return;
    }
    serve(req, res, finalhandler(req, res));
  });
  // ...
}

export async function createWebUIHandler(options: Pick<WebUIOptions, 'rpc'>) {
  return async (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => {
    if (!req.url) return false;

    try {
      const url = new URL(req.url, `http://${req.headers.host}`);
      if (url.pathname === '/_/open') {
        return await handleWebUIOpenRequest(url, req, res);
      }
      return false;
    } catch (error) {
      return false;
    }
  };
}

export async function handleWebUIOpenRequest(
  url: URL,
  req: IncomingMessage,
  res: ServerResponse<IncomingMessage>
) {
  try {
    const auth = req.headers.authorization;
    const dir = url.searchParams.get('dir');    
    if (dir) {
      const open = (await import('open')).default;
      await open(dir).catch(() => {});
      res.write(JSON.stringify({ status: 'OK', open: p }));
    }

    res.setHeader('Content-Type', 'application/json');
    res.end();
    return true;
  } catch (error) {
    return false;
  }
}

我们给本来的静态文件 HTTP 服务,添加一个用于打开文件的接口 /_/open?dir=...。前端给这个接口发请求,实现的效果就是打开文件资源管理器。

可能存在一些安全问题,实际可以额外验证一下令牌,或者使用选项禁用此功能。

代理 aria2 RPC 端口#

虽然不太需要这么做,但是或许我们可以把这个页面部署到服务器上。那么为此,我们就需要在公网上给它开 2 个端口,一个用于前端应用,一个用于 aria2 RPC 服务;或者需要额外的配置,或者使用 nginx 等等来发现 RPC 服务的位置。

和上一节一样,既然我们已经有了一个后端服务,那么我们为什么不充分利用它呢?

直接为这个服务的 /jsonrpc 路由,添加一个转发到 aria2 RPC 服务的代理。于是,部署只需要暴露应用的位置即可,连接 RPC 服务只需要访问同源的 /jsonrpc 即可。

export async function createWebUIHandler(options: Pick<WebUIOptions, 'rpc'>) {
  // 创建一个代理中间件
  const { createProxyMiddleware } = await import('http-proxy-middleware');
  const proxyMiddleware = createProxyMiddleware({
    target: `http://127.0.0.1:${options.rpc.port}`,
    changeOrigin: false,
    ws: true,
    logLevel: 'silent'
  });

  return async (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => {
    if (!req.url) return false;

    try {
      const url = new URL(req.url, `http://${req.headers.host}`);
      if (url.pathname === '/jsonrpc') {
        // 转发请求
        proxyMiddleware(req as any, res as any, () => {});
        return true;
      } else if (url.pathname === '/_/open') {
        return await handleWebUIOpenRequest(url, req, res);
      }
      return false;
    } catch (error) {
      return false;
    }
  };
}

基于 http-proxy-middleware,完成 HTTP 请求的代理转发工作,它这个库还支持转发 WebSocket 请求,直接使用即可。

迷跡波#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.