Hoppyscotsch Desktop 的 HTTP 执行引擎:tauri-plugin-relay 插件设计与使用详解
本文围绕 Hoppyscotsch 桌面版内嵌的 Tauri 插件 tauri-plugin-relay 展开,讲清这个“HTTP 请求-响应中转”插件的能力边界(自定义头、证书、代理、多认证方式、内容类型、异步取消)、在 Tauri 2.0 应用中的安装与初始化方式,以及它从前端 JS 调用到 Rust 底层执行的完整链路。读完后,你能独立为 Tauri 应用接入一个基于 libcurl 的原生 HTTP 客户端,并理解 Hoppyscotsch 桌面端请求执行与取消的底层机制。
插件定位:为什么 Tauri 应用需要 HTTP 中继
浏览器/WebView 里的 fetch 受 CORS、证书策略与代理能力限制。tauri-plugin-relay 是一个面向 Tauri 应用的 HTTP request-response relay 插件,把真实网络请求下沉到原生 Rust 侧执行,提供 README 中列出的核心能力:
- HTTP client 构建在 libcurl 之上;
- SSL/TLS 证书管理(客户端证书、自定义 CA);
- 代理支持(含代理认证);
- 多种认证方式(Basic、Bearer、Digest);
- 多种请求体内容处理(文本、JSON、表单、二进制);
- 异步请求执行与取消(cancellation)支持。
在 Hoppyscotsch 仓库中,该插件以插件工作区成员的形式存放在 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/。桌面端主工程通过 git 依赖锁定到指定 rev 引入它,见 src-tauri/Cargo.toml:
tauri-plugin-relay = { git = "https://github.com/CuriousCorrelation/tauri-plugin-relay", rev = "273488c8f50a22ee707af6b50ccd5570851f8bc9" }
并在应用构建链中注册插件,见 src-tauri/src/lib.rs:
.plugin(tauri_plugin_relay::init())
这与插件 README 中给出的 Quick Start 完全一致,可以确认桌面端确实以“插件初始化 + 前端 invoke”的模式消费该中继能力。
安装与依赖声明
该插件要求 Tauri 2.0 或更高版本。
按 README 的说明,插件支持直接从 GitHub 以 git 依赖方式安装。Rust 侧:
[dependencies]
tauri-plugin-relay = { git = "https://github.com/CuriousCorrelation/tauri-plugin-relay" }
前端 JS/TS 侧:
"dependencies": {
"@CuriousCorrelation/plugin-relay": "github:CuriousCorrelation/tauri-plugin-relay"
}
插件自身的 Cargo.toml 声明了关键依赖与版本下限:tauri = "2.1.0"、serde、thiserror = "2"、tracing,以及真正干活的核心 crate relay(同为 git 依赖),rust-version = "1.77.2"。也就是说,插件本体只是一层薄薄的 Tauri 适配层,真正的 HTTP 执行逻辑在独立的 relay crate 中。仓库内另有一个 packages/hoppscotch-relay Rust 工作区包,属于同一套中继基础设施,可作为深入阅读底层实现的入口。
前端包的 package.json 名为 @CuriousCorrelation/plugin-relay,产物在 dist-js/(同时提供 ESM index.js 与 CJS index.cjs,类型定义在 index.d.ts),依赖 @tauri-apps/api 2.1.1,构建脚本为 tsc && rollup -c。
插件初始化:init() 到底做了什么
Rust 侧的入口是 src/lib.rs 中的 init():
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("relay")
.invoke_handler(tauri::generate_handler![
commands::execute,
commands::cancel
])
.setup(|app, api| {
#[cfg(mobile)]
{ let relay = mobile::init(app, api)?; app.manage(relay); }
#[cfg(desktop)]
{ let relay = desktop::init(app, api)?; app.manage(relay); }
Ok(())
})
.build()
}
从源码结构看,init() 做了三件事:
- 注册命令:通过
generate_handler!暴露且仅暴露两个可被前端调用的命令——execute与cancel,对应 src/commands.rs; - 平台分支:按
#[cfg(desktop)]/#[cfg(mobile)]条件编译分别初始化 desktop.rs 或 mobile.rs 中的Relay实现,并app.manage(relay)存入 Tauri 状态; - 提供状态访问器:
RelayExttrait(lib.rs)让任何实现了Manager的对象都能通过app.relay()取出Relay状态实例。
desktop 平台的 Relay 实现(src/desktop.rs)本身很薄:
pub struct Relay<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Relay<R> {
pub async fn execute(&self, request: RunRequest) -> Result<ExecuteResponse> {
match relay::execute(request).await {
Ok(response) => Ok(ExecuteResponse::Success { response }),
Err(error) => Ok(ExecuteResponse::Error { error }),
}
}
pub async fn cancel(&self, request_id: CancelRequest) -> Result<CancelResponse> {
if let Err(e) = relay::cancel(request_id).await {
return Err(e.into());
}
Ok(())
}
}
可以看到执行路径为:前端 invoke → Tauri 命令 → Relay::execute → relay::execute(request)(relay crate 真正发起 libcurl 请求)。一个值得注意的设计是:HTTP 执行失败(网络错误、证书错误等)不会让命令返回 Err,而是包装成 ExecuteResponse::Error 正常返回;只有取消失败这类“插件自身故障”才会走 Err 分支。
前端调用:execute 与 cancel
插件的前端 API 只有两个函数,定义在 guest-js/index.ts:
export async function execute(request: Request): Promise<RequestResult> {
return await invoke<RequestResult>('plugin:relay|execute', { request })
}
export async function cancel(requestId: number): Promise<void> {
return await invoke<void>('plugin:relay|cancel', { requestId })
}
即通过 Tauri 的命令通道 plugin:relay|execute / plugin:relay|cancel 与 Rust 侧通信。README 给出的最简示例:
import { execute, cancel } from '@CuriousCorrelation/plugin-relay'
// Execute a request
const result = await execute({
id: 1,
url: "https://api.example.com/data",
method: "POST",
headers: {
"Content-Type": ["application/json"]
},
content: {
kind: "json",
content: { hello: "world" }
}
})
// Cancel a request
await cancel(1)
request.id 是请求的唯一标识:execute 传入的 id 会随响应原样带回,cancel(id) 则凭此 id 中断仍在飞行中的请求。返回的 RequestResult 是一个带 kind 标签的联合类型:
export type RequestResult =
| { kind: 'success'; response: Response }
| { kind: 'error'; error: RelayError }
与 Rust 侧 src/models.rs 的 ExecuteResponse(#[serde(tag = "kind")],序列化为 "success" / "error")一一对应。
请求类型系统:Method、Version、Content 与 Auth
guest-js/index.ts 定义了完整的请求类型体系,这是插件最“干货”的部分。
方法与版本:Method 覆盖 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/CONNECT/TRACE 九个值;Version 支持 HTTP/1.0、HTTP/1.1、HTTP/2.0、HTTP/3.0,说明底层中继在 libcurl 之上可协商现代协议。
内容类型:README 的 Content Types 表格列出了五类请求体:
| 类型 | 说明 |
|---|---|
text |
纯文本内容 |
json |
JSON 数据,自动解析 |
form |
支持文件上传的 multipart 表单数据 |
binary |
原始二进制数据,可带 MIME 类型 |
urlencoded |
URL 编码表单数据 |
而 guest-js/index.ts 的 ContentType 联合类型比表格更丰富,还包含 xml、multipart 与 stream:
export type ContentType =
| { kind: "text"; content: string; mediaType: ... }
| { kind: "json"; content: unknown; mediaType: MediaType.APPLICATION_JSON | MediaType.APPLICATION_LD_JSON }
| { kind: "xml"; content: string; mediaType: ... }
| { kind: "form"; content: FormData; mediaType: MediaType.APPLICATION_FORM }
| { kind: "binary"; content: Uint8Array; mediaType: ...; filename?: string }
| { kind: "multipart"; content: FormData; mediaType: MediaType.MULTIPART_FORM }
| { kind: "urlencoded"; content: string; mediaType: ... }
| { kind: "stream"; content: ReadableStream; mediaType: string }
其中 FormData 的值为 [string, FormDataValue[]][],FormDataValue 支持 text 或 file(带 filename、contentType 与 Uint8Array 数据),因此 multipart 上传是完整建模的。
认证方式:README 的 Authentication 表格列出三种:
| 方式 | 说明 |
|---|---|
basic |
Basic HTTP 认证 |
bearer |
Bearer Token 认证 |
digest |
Digest 认证(MD5、SHA-256、SHA-512) |
AuthType 类型(guest-js/index.ts)还进一步扩展了 oauth2(authorization_code / client_credentials / password / implicit 四种 grant,可携带 accessToken / refreshToken)、apikey(header 或 query 注入)与 aws(SigV4 风格的 accessKey/secretKey/region/service 签名,可加 sessionToken)等变体,与 Hoppyscotsch 主产品的认证能力面保持一致。
完整 Request 结构:
export interface Request {
id: number
url: string
method: Method
version: Version
headers?: Record<string, string>
params?: Record<string, string>
content?: ContentType
auth?: AuthType
security?: {
certificates?: {
client?: CertificateType
ca?: Array<Uint8Array>
}
verifyHost?: boolean
verifyPeer?: boolean
}
proxy?: {
url: string
auth?: { username: string; password: string }
}
meta?: RequestMeta
}
RequestOptions(经 meta.options 传入)提供请求级行为开关:timeout、followRedirects、maxRedirects、decompress、cookies、keepAlive(guest-js/index.ts)。
安全与代理配置
README 的 Security 一节指出插件提供四项安全能力:客户端证书支持(PEM、PKCS#12)、自定义 CA 证书、证书校验控制、主机验证设置。对应到类型定义:
export type CertificateType =
| { kind: "pem"; cert: Uint8Array; key: Uint8Array }
| { kind: "pfx"; data: Uint8Array; password: string }
security.certificates.client 接受 PEM(证书 + 私钥各一段字节)或 PFX/PKCS#12(整包字节 + 密码)两种形态;security.certificates.ca 是 CA 证书字节数组,用于私有 PKI 场景的信任锚扩展;verifyHost / verifyPeer 分别控制主机名验证与证书链验证,用于自签名证书等开发/内网调试场景。代理则通过 proxy.url 指定,可选 proxy.auth 提供代理端 Basic 认证。
响应结构与错误处理
成功时前端拿到的 Response(guest-js/index.ts)包含:状态码(StatusCode 联合类型穷举了 1xx~5xx 常用状态码)、statusText、HTTP 版本、响应头、可选的 cookie 列表(含 domain、path、expires、secure、httpOnly、sameSite)、body(Uint8Array + mediaType),以及 meta 元数据:
meta: {
timing: { start: number; end: number },
size: { headers: number; body: number; total: number }
}
也就是说,计时与体积统计(头/体/总字节)由中继层直接产出,前端无需自行掐表,这对 Hoppyscotsch 界面里的响应耗时展示来说是关键支撑。
失败时的 RelayError 是五类加一类 unsupported 的标签联合:
export type RelayError =
| UnsupportedFeatureError
| { kind: "network"; message: string; cause?: unknown }
| { kind: "timeout"; message: string; phase?: "connect" | "tls" | "response" }
| { kind: "certificate"; message: string; cause?: unknown }
| { kind: "parse"; message: string; cause?: unknown }
| { kind: "abort"; message: string }
这与 README Error Handling 一节列出的“网络失败、证书问题、超时场景、解析错误、请求取消”五类详细错误信息逐条对应;timeout 还细分了 connect / tls / response 三个阶段,便于定位是连接慢、TLS 握手慢还是等响应体慢。unsupported_feature 则用于某特性(如 stream 内容)在当前平台中继实现中不可用的情况。
Rust 侧的顶层 Error 枚举定义在 src/error.rs:io::Error 透传、relay::error::RelayError 透传(thiserror::Error 派生),移动端还有 PluginInvokeError。其自定义 Serialize 实现把错误序列化为字符串传给前端。而 ExecuteResponse::Error { error } 路径则会把 RelayError 结构化地序列化给前端,两者配合覆盖了“插件级故障”与“请求级失败”两个层次。
权限声明
Tauri 2.0 插件通过 TOML 声明权限。本插件的默认权限组在 permissions/default.toml:
[default]
description = "Default permissions for the plugin"
permissions = ["allow-execute", "allow-cancel"]
即默认放行 execute 与 cancel 两个命令;permissions/autogenerated/ 目录下还有按命令粒度自动生成的 execute.toml、cancel.toml 及 schema.json,宿主应用可在 capabilities 中按需收紧。
开发环境与相关 crate
按 README Development 一节,本地开发该插件需要:Rust 1.77.2+(与 Cargo.toml 的 rust-version 一致)、Node.js 18+、pnpm、以及带 SSL 支持的 libcurl。插件目录内还配有 devenv.nix / devenv.yaml,可在 Nix devenv 环境中获取一致的构建依赖。
值得对照阅读的相关代码:
- src/commands.rs:两个 Tauri 命令入口,含
tracing日志埋点; - src/desktop.rs:桌面平台
Relay到relaycrate 的转发逻辑; - guest-js/index.ts:前端完整类型定义(方法与状态码的语义注释、内容/认证/证书/代理的全部形态);
- packages/hoppscotch-relay/src/:仓库内的 relay 工作区包,展示同一套中继机制在 Rust 侧的组成(
relay.rs、interop.rs、error.rs)。
小结
tauri-plugin-relay 是 Hoppyscotsch 桌面端请求执行的原生层:Tauri 命令通道只做转发(execute / cancel),libcurl 承担网络能力,而类型系统在前端(guest-js/index.ts)与 Rust(relay crate 的 RelayRequest/RelayResponse)之间保持 serde 双端对齐。理解 README 中的能力清单,再对照 lib.rs 的插件装配与 desktop.rs 的转发实现,就能完整把握“前端一条 execute 调用如何变成一次可控、可取消、可携带证书与代理配置的原生 HTTP 请求”这一完整链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00