feat: OpsxAPI v1.0 全协议接口工作台
- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
No files matched your search
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "opsxapi"
|
||||
version = "0.1.0"
|
||||
description = "OpsxAPI — API workbench for ops & developers"
|
||||
authors = ["OpsxAPI"]
|
||||
edition = "2021"
|
||||
default-run = "OpsxAPI"
|
||||
|
||||
[lib]
|
||||
name = "opsxapi_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "OpsxAPI"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
url = "2"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the OpsxAPI main window",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 950 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0B182C"/>
|
||||
<stop offset="100%" stop-color="#0F203A"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="28" y="28" width="456" height="456" rx="110" fill="url(#g)"/>
|
||||
<rect x="28" y="28" width="456" height="70" rx="110" fill="#0EA5E9"/>
|
||||
<text x="256" y="290" text-anchor="middle" font-family="SF Pro Display, Helvetica, Arial" font-size="140" font-weight="700" fill="#FFFFFF">API</text>
|
||||
<text x="256" y="360" text-anchor="middle" font-family="SF Pro Display, Helvetica, Arial" font-size="42" fill="#94A3B8">Opsx</text>
|
||||
<circle cx="110" cy="400" r="16" fill="#10B981"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 445 B |
|
After Width: | Height: | Size: 774 B |
|
After Width: | Height: | Size: 774 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 987 B |
|
After Width: | Height: | Size: 987 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 957 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,136 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BodySpec {
|
||||
mode: String,
|
||||
raw: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HttpSpec {
|
||||
method: String,
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
headers: HashMap<String, String>,
|
||||
query: Option<HashMap<String, String>>,
|
||||
body: Option<BodySpec>,
|
||||
timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HttpResult {
|
||||
status: u16,
|
||||
headers: HashMap<String, String>,
|
||||
body: String,
|
||||
time_ms: u128,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WsSpec {
|
||||
url: String,
|
||||
protocols: Option<Vec<String>>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
fn validate_http_url(raw: &str) -> Result<url::Url, String> {
|
||||
let parsed = url::Url::parse(raw).map_err(|e| format!("invalid URL: {e}"))?;
|
||||
match parsed.scheme() {
|
||||
"http" | "https" => {}
|
||||
_ => return Err("only http/https schemes are allowed".into()),
|
||||
}
|
||||
match parsed.host_str().unwrap_or("").to_ascii_lowercase().as_str() {
|
||||
"169.254.169.254" | "metadata.google.internal" => {
|
||||
return Err("blocked metadata host".into())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn http_send(spec: HttpSpec) -> Result<HttpResult, String> {
|
||||
let mut url = validate_http_url(&spec.url)?;
|
||||
if let Some(query) = &spec.query {
|
||||
let mut pairs = url.query_pairs_mut();
|
||||
for (key, value) in query {
|
||||
pairs.append_pair(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let method = reqwest::Method::from_bytes(spec.method.as_bytes())
|
||||
.map_err(|e| format!("invalid method: {e}"))?;
|
||||
let timeout = Duration::from_millis(spec.timeout_ms.unwrap_or(30_000));
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut request = client.request(method, url);
|
||||
for (key, value) in spec.headers {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
if let Some(body) = spec.body {
|
||||
if body.mode != "none" {
|
||||
request = request.body(body.raw.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
let started = Instant::now();
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
let status = response.status().as_u16();
|
||||
let headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
let size = bytes.len();
|
||||
let body = String::from_utf8_lossy(&bytes).into_owned();
|
||||
Ok(HttpResult {
|
||||
status,
|
||||
headers,
|
||||
body,
|
||||
time_ms: started.elapsed().as_millis(),
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ws_connect(spec: WsSpec) -> Result<String, String> {
|
||||
let _ = (&spec.protocols, &spec.headers);
|
||||
// TODO(week2): hold WebSocket sessions in managed state and emit messages.
|
||||
Err(format!(
|
||||
"WebSocket native tunnel is not implemented yet for {} (use browser WS in MVP)",
|
||||
spec.url
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ws_send(session_id: String, data: String) -> Result<(), String> {
|
||||
let _ = (session_id, data);
|
||||
Err("WebSocket native send is a Week 2 stub".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ws_close(session_id: String) -> Result<(), String> {
|
||||
let _ = session_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
http_send,
|
||||
ws_connect,
|
||||
ws_send,
|
||||
ws_close
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running OpsxAPI desktop");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
opsxapi_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpsxAPI",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.opsx.api",
|
||||
"build": {
|
||||
"beforeDevCommand": "bash -lc 'curl -fsS http://127.0.0.1:5173 >/dev/null 2>&1 || npm --prefix ../.. run dev:web'",
|
||||
"devUrl": "http://127.0.0.1:5173",
|
||||
"beforeBuildCommand": "npm --prefix ../.. run build:web",
|
||||
"frontendDist": "../../web/dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "OpsxAPI",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"label": "main"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "OpsxAPI — API workbench",
|
||||
"longDescription": "OpsxAPI is an API workbench for ops and developers: HTTP / WebSocket, one React UI, Web and Desktop share Transport."
|
||||
}
|
||||
}
|
||||