rinova-proxy-sdk 1.0.0

Proxy subscription to Clash config converter — SDK
Documentation

rinova-proxy-sdk

Proxy subscription to Clash config converter — Rust SDK.

v1.0.0 · Author: Rina

Install

[dependencies]

rinova-proxy-sdk = "1.0.0"

tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

cargo add rinova-proxy-sdk

Quick Start

use rinova_proxy_sdk::convert;

#[tokio::main]
async fn main() -> Result<(), rinova_proxy_sdk::ProxyError> {
    let result = convert("https://jms-subscription-url", None).await?;
    println!("parsed {} nodes", result.nodes.len());
    std::fs::write("clash.yaml", result.yaml)?;
    Ok(())
}

API Reference

convert(url, opts) -> Result<ConvertResult>

Fetch a subscription URL, parse proxy nodes, and generate a complete Clash configuration.

use rinova_proxy_sdk::{convert, ConvertOptions, RuleMode};

// Basic
let result = convert("https://jms-api.example.com/sub?token=xxx", None).await?;

// With options
let result = convert(
    "https://...",
    Some(ConvertOptions {
        rules: RuleMode::External,  // default: RuleMode::Builtin
        deduplicate: false,         // default: true
    }),
).await?;

let rinova_proxy_sdk::ConvertResult { yaml, config, nodes } = result;
Parameter Type Default Description
url &str required Proxy subscription URL
opts.rules RuleMode Builtin Builtin or External (ACL4SSR RuleSet)
opts.deduplicate bool true Append -2, -3 to duplicate node names

Returns ConvertResult:

Field Type Description
yaml String Clash config as YAML
config ClashConfig Full config object
nodes Vec<ProxyNode> Parsed proxy nodes

Errors ProxyError if subscription is empty or no valid nodes are found.


convert_from_lines(lines, opts) -> Result<ConvertResult>

Convert URI lines without network requests.

use rinova_proxy_sdk::{convert_from_lines, ConvertOptions, RuleMode};

let lines = [
    "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#US-01",
    "trojan://pass@sg.example.com:443?sni=sg.example.com#SG-01",
];

let result = convert_from_lines(
    &lines,
    Some(ConvertOptions {
        rules: RuleMode::External,
        deduplicate: true,
    }),
)?;

Same options and return type as convert().


parse_uri(uri) -> Result<ProxyNode>

Parse a single proxy URI.

use rinova_proxy_sdk::parse_uri;

let node = parse_uri("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@host:8388#MyNode")?;
assert_eq!(node.name, "MyNode");
assert_eq!(node.proxy_type.as_str(), "ss");
assert_eq!(node.port, 8388);

// Protocol-specific fields live in `extra`
let cipher = node.extra.get("cipher").and_then(|v| v.as_str());
let password = node.extra.get("password").and_then(|v| v.as_str());

Supported prefixes: ss://, vmess://, trojan://, hysteria2://, hy2://


parse_lines(lines) -> Vec<ProxyNode>

Parse multiple URI lines. Invalid lines are skipped with a warning to stderr.

use rinova_proxy_sdk::parse_lines;

let nodes = parse_lines(&[
    "ss://YWVz...@host1:8388#Node1",
    "invalid-uri",              // skipped
    "trojan://pass@host2:443#Node2",
]);
assert_eq!(nodes.len(), 2);

fetch_subscription(url) -> Result<Vec<String>>

Fetch and decode a subscription into URI lines (Base64 / URL-decode tolerant).

use rinova_proxy_sdk::fetch_subscription;

#[tokio::main]
async fn main() -> Result<(), rinova_proxy_sdk::ProxyError> {
    let lines = fetch_subscription("https://jms-api.example.com/sub").await?;
    for line in &lines {
        println!("{line}");
    }
    Ok(())
}

User-Agent: @rinova/proxy-sdk/1.0.0


deduplicate_names(nodes: &mut [ProxyNode])

Rename duplicate nodes in-place: first keeps original name, others get -2, -3, …

use rinova_proxy_sdk::{deduplicate_names, ProxyNode, ProxyType};

let mut nodes = vec![
    ProxyNode::new("Japan".into(), ProxyType::Ss, "a.com".into(), 443),
    ProxyNode::new("Japan".into(), ProxyType::Ss, "b.com".into(), 443),
];
deduplicate_names(&mut nodes);
// → "Japan", "Japan-2"

build_config(nodes, rule_mode) -> ClashConfig

Build a Clash config from parsed nodes.

use rinova_proxy_sdk::{build_config, parse_lines, RuleMode};

let nodes = parse_lines(&["ss://..."]);
let config = build_config(&nodes, RuleMode::Builtin);
// config.proxies, config.proxy_groups, config.rules

Policy groups (ACL4SSR-style chained routing):

Rule MATCH / foreign domains
    ↓
🌍 国外网站  (default: 🚀 节点选择)
    ↓
🚀 节点选择  ← switch nodes in Verge
    ↓
specific proxy node
Group Purpose
🚀 节点选择 Main node selector
♻️ 自动选择 url-test, lowest latency
🎯 直连 Direct or proxy
🌍 国外网站 Referenced by rules, follows 🚀 节点选择
🇨🇳 国内网站 Domestic routing
🛑 广告拦截 REJECT

to_yaml(config) -> Result<String>

Serialize ClashConfig to YAML.

use rinova_proxy_sdk::{build_config, to_yaml, RuleMode};

let yaml = to_yaml(&build_config(&nodes, RuleMode::Builtin))?;
std::fs::write("clash.yaml", yaml)?;

start_server(opts) -> Result<ServerHandle>

Start an HTTP server with automatic subscription refresh.

use rinova_proxy_sdk::{start_server, ServerOptions, RuleMode};

#[tokio::main]
async fn main() -> Result<(), rinova_proxy_sdk::ProxyError> {
    let handle = start_server(ServerOptions {
        url: "https://jms-api.example.com/sub".into(),
        port: 25500,
        interval_min: 60,
        rule_mode: RuleMode::Builtin,
    }).await?;

    // Block until Ctrl+C / SIGTERM
    tokio::signal::ctrl_c().await.ok();
    handle.shutdown().await;
    Ok(())
}

HTTP endpoints (listen on 127.0.0.1:{port}):

Method Path Description
GET /clash.yaml Clash config YAML
GET /health JSON: status, nodes, updatedAt, nextRefreshMin, lastError
POST /refresh Manual refresh → { ok, skipped, nodes }
GET /refresh 405 Method Not Allowed

All responses include Access-Control-Allow-Origin: *.


t(key, params) / t_with_fallback(key, params, fallback) / get_lang()

Localized messages based on LANG / LC_ALL.

use rinova_proxy_sdk::{t, t_with_fallback, get_lang, Lang};

match get_lang() {
    Lang::Zh => println!("中文环境"),
    Lang::En => println!("English"),
}

println!("{}", t("refreshing", &[]));
println!("{}", t("parsed", &[("count", "5")]));
println!("{}", t_with_fallback("missing_key", &[], Some("fallback text")));

Types

use rinova_proxy_sdk::{
    ClashConfig, ConvertOptions, ConvertResult,
    ProxyGroup, ProxyNode, ProxyType, RuleMode,
    ProxyError, Result,
};
Type Description
ProxyNode name, proxy_type, server, port, extra (protocol fields)
ProxyGroup Policy group (name, group_type, proxies, …)
ClashConfig Full config (proxies, proxy_groups, rules, ports)
ConvertOptions { rules: RuleMode, deduplicate: bool }
ConvertResult { config, yaml, nodes }
RuleMode Builtin | External
ProxyType Ss | Vmess | Trojan | Hysteria2
ServerOptions { url, port, interval_min, rule_mode }
ServerHandle Call .shutdown().await to stop
Lang En | Zh
ProxyError Error enum
Result<T> std::result::Result<T, ProxyError>

ProxyNode.extra fields (by protocol)

Protocol-specific options are stored in extra (IndexMap<String, Value>):

Protocol Common keys in extra
SS cipher, password
VMess uuid, alterId, cipher, network, tls, sni, ws-opts, grpc-opts, …
Trojan password, sni, skip-cert-verify
Hysteria2 password, sni, alpn, up, down, skip-cert-verify

Modules

Module Exports
crate root convert, convert_from_lines, re-exports
parser parse_uri, parse_lines
fetch fetch_subscription, deduplicate_names
builder build_config, to_yaml
server start_server, ServerHandle, ServerOptions
i18n t, t_with_fallback, get_lang, Lang
types All public types
error ProxyError, Result

License

MIT — Copyright (c) 2026 Rina