quip-node-manager

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Quip Node Manager

Quip Node Manager

Skill by ara.so — Daily 2026 Skills collection.
Quip Node Manager is a cross-platform desktop application (macOS, Linux, Windows) built with Tauri v2 + Rust that lets you run, configure, and monitor Quip Network nodes. It supports two execution modes — Docker (default on Windows/Linux) and Native binary (default on macOS) — with a GUI front-end and an optional terminal UI (
--cli
flag).

ara.so开发的Skill——属于Daily 2026 Skills合集。
Quip Node Manager是一款基于Tauri v2 + Rust构建的跨平台桌面应用(支持macOS、Linux、Windows),可用于运行、配置和监控Quip Network节点。它支持两种执行模式——Docker(Windows/Linux默认模式)和Native binary(macOS默认模式),提供GUI前端和可选的终端UI(使用
--cli
参数)。

Installation

安装

Quick Install (macOS / Linux)

快速安装(macOS / Linux)

sh
curl -fsSL https://gitlab.com/quip.network/quip-node-manager/-/raw/main/scripts/install.sh | sh
sh
curl -fsSL https://gitlab.com/quip.network/quip-node-manager/-/raw/main/scripts/install.sh | sh

Quick Install (Windows PowerShell)

快速安装(Windows PowerShell)

powershell
irm https://gitlab.com/quip.network/quip-node-manager/-/raw/main/scripts/install.ps1 | iex
powershell
irm https://gitlab.com/quip.network/quip-node-manager/-/raw/main/scripts/install.ps1 | iex

Manual Installation

手动安装

PlatformFormatSteps
macOS
.dmg
Open DMG, drag to
/Applications
, then run
xattr -dr com.apple.quarantine "/Applications/Quip Node Manager.app"
Linux
.AppImage
chmod +x quip-node-manager-linux-x86_64.AppImage && ./quip-node-manager-linux-x86_64.AppImage
Linux
.deb
sudo dpkg -i quip-node-manager-linux-x86_64.deb
Windows
.exe
Run installer; click More info → Run anyway if SmartScreen warns

平台格式步骤
macOS
.dmg
打开DMG文件,将应用拖至
/Applications
目录,然后运行
xattr -dr com.apple.quarantine "/Applications/Quip Node Manager.app"
Linux
.AppImage
执行
chmod +x quip-node-manager-linux-x86_64.AppImage && ./quip-node-manager-linux-x86_64.AppImage
Linux
.deb
执行
sudo dpkg -i quip-node-manager-linux-x86_64.deb
Windows
.exe
运行安装程序;若SmartScreen发出警告,点击更多信息 → 仍要运行

Development Setup

开发环境搭建

Prerequisites

前置条件

Clone and Run

克隆并运行

sh
git clone https://gitlab.com/quip.network/quip-node-manager.git
cd quip-node-manager

bun install          # Install JS dependencies
bun run dev          # Launch hot-reloading dev build (opens desktop window)
bun run build        # Production build for current platform
sh
git clone https://gitlab.com/quip.network/quip-node-manager.git
cd quip-node-manager

bun install          # 安装JS依赖
bun run dev          # 启动热重载开发构建(打开桌面窗口)
bun run build        # 为当前平台构建生产版本

Rust-only checks (no frontend needed)

仅Rust检查(无需前端)

sh
cd src-tauri
cargo check          # Fast type-check
cargo clippy         # Lint with suggestions
cargo test           # Run unit tests

sh
cd src-tauri
cargo check          # 快速类型检查
cargo clippy         # 代码检查并给出建议
cargo test           # 运行单元测试

Architecture

架构

quip-node-manager/
├── src/                    # Frontend: vanilla HTML/CSS/JS
│   ├── index.html
│   ├── main.js             # Tauri IPC calls (window.__TAURI__)
│   └── styles.css
├── src-tauri/
│   ├── src/
│   │   ├── main.rs         # App entry point
│   │   ├── commands.rs     # Tauri IPC command handlers
│   │   ├── node.rs         # Node process management
│   │   ├── config.rs       # TOML config generation
│   │   ├── docker.rs       # Docker mode logic
│   │   ├── updater.rs      # Background update monitor
│   │   └── gpu.rs          # CUDA/Metal device detection
│   ├── Cargo.toml
│   └── tauri.conf.json
├── scripts/
│   ├── install.sh
│   └── install.ps1
└── AGENTS.md               # Detailed architecture docs
Data directory:
~/quip-data/
— stores settings, TOML config, secrets, downloaded binaries, and trust database.

quip-node-manager/
├── src/                    # 前端:原生HTML/CSS/JS
│   ├── index.html
│   ├── main.js             # Tauri IPC调用(window.__TAURI__)
│   └── styles.css
├── src-tauri/
│   ├── src/
│   │   ├── main.rs         # 应用入口
│   │   ├── commands.rs     # Tauri IPC命令处理器
│   │   ├── node.rs         # 节点进程管理
│   │   ├── config.rs       # TOML配置生成
│   │   ├── docker.rs       # Docker模式逻辑
│   │   ├── updater.rs      # 后台更新监控
│   │   └── gpu.rs          # CUDA/Metal设备检测
│   ├── Cargo.toml
│   └── tauri.conf.json
├── scripts/
│   ├── install.sh
│   └── install.ps1
└── AGENTS.md               # 详细架构文档
数据目录:
~/quip-data/
—— 存储设置、TOML配置、密钥、下载的二进制文件和信任数据库。

Key Concepts

核心概念

Run Modes

运行模式

ModeDefault OnHow it works
DockerWindows, LinuxPulls/manages a container image via Docker daemon
NativemacOSDownloads a standalone quip-protocol binary
模式默认平台工作原理
DockerWindows、Linux通过Docker守护进程拉取/管理容器镜像
NativemacOS下载独立的quip-protocol二进制文件

Pre-flight Checklist

预启动检查清单

Before starting the node, the manager verifies:
  1. Docker available (Docker mode) or binary downloaded (Native mode)
  2. Node secret exists in
    ~/quip-data/
  3. Public IP reachable
  4. Port forwarding configured
  5. Firewall rules allow node traffic

启动节点前,管理器会验证:
  1. Docker可用(Docker模式)或二进制文件已下载(Native模式)
  2. 节点密钥存在于
    ~/quip-data/
  3. 公网IP可访问
  4. 端口转发已配置
  5. 防火墙规则允许节点流量

Tauri IPC Commands (Frontend → Backend)

Tauri IPC命令(前端 → 后端)

The frontend calls Rust commands via
window.__TAURI__.core.invoke
. Key commands:
js
const { invoke } = window.__TAURI__.core;

// Get current node status
const status = await invoke('get_node_status');
// Returns: { running: bool, mode: 'docker'|'native', uptime_secs: number }

// Start the node
await invoke('start_node');

// Stop the node
await invoke('stop_node');

// Get configuration
const config = await invoke('get_config');

// Save configuration
await invoke('save_config', { config: { /* see config schema below */ } });

// Run pre-flight checks
const checks = await invoke('run_preflight');
// Returns: { docker: bool, secret: bool, public_ip: string, port_open: bool, firewall: bool }

// Get GPU devices
const gpus = await invoke('get_gpu_devices');
// Returns: [{ id: string, name: string, type: 'cuda'|'metal', enabled: bool, utilization: number }]

// Check for updates
const update = await invoke('check_for_updates');
// Returns: { app: string|null, node: string|null, docker_image: string|null }

前端通过
window.__TAURI__.core.invoke
调用Rust命令。核心命令:
js
const { invoke } = window.__TAURI__.core;

// 获取当前节点状态
const status = await invoke('get_node_status');
// 返回值:{ running: bool, mode: 'docker'|'native', uptime_secs: number }

// 启动节点
await invoke('start_node');

// 停止节点
await invoke('stop_node');

// 获取配置
const config = await invoke('get_config');

// 保存配置
await invoke('save_config', { config: { /* 见下方配置 schema */ } });

// 运行预启动检查
const checks = await invoke('run_preflight');
// 返回值:{ docker: bool, secret: bool, public_ip: string, port_open: bool, firewall: bool }

// 获取GPU设备
const gpus = await invoke('get_gpu_devices');
// 返回值:[{ id: string, name: string, type: 'cuda'|'metal', enabled: bool, utilization: number }]

// 检查更新
const update = await invoke('check_for_updates');
// 返回值:{ app: string|null, node: string|null, docker_image: string|null }

Configuration Schema (TOML)

配置Schema(TOML)

The app generates a TOML config written to
~/quip-data/config.toml
matching the quip-protocol format:
toml
[node]
mode = "native"          # or "docker"
secret_path = "/Users/you/quip-data/secret"
data_dir = "/Users/you/quip-data"

[network]
public_ip = "1.2.3.4"
port = 9000
tls_cert = "/path/to/cert.pem"   # optional
tls_key  = "/path/to/key.pem"    # optional

[gpu]
enabled = true
devices = ["0", "1"]
utilization = 80         # percent
yield_mode = false

[dwave]
enabled = false
token = ""               # set via env: DWAVE_TOKEN
daily_budget_seconds = 60

[updates]
auto_restart_on_image_update = true
check_interval_secs = 1800

应用会生成符合quip-protocol格式的TOML配置,并写入
~/quip-data/config.toml
toml
[node]
mode = "native"          # 或 "docker"
secret_path = "/Users/you/quip-data/secret"
data_dir = "/Users/you/quip-data"

[network]
public_ip = "1.2.3.4"
port = 9000
tls_cert = "/path/to/cert.pem"   # 可选
tls_key  = "/path/to/key.pem"    # 可选

[gpu]
enabled = true
devices = ["0", "1"]
utilization = 80         # 百分比
yield_mode = false

[dwave]
enabled = false
token = ""               # 通过环境变量DWAVE_TOKEN设置
daily_budget_seconds = 60

[updates]
auto_restart_on_image_update = true
check_interval_secs = 1800

Rust Backend Patterns

Rust后端模式

Adding a Tauri Command

添加Tauri命令

rust
// src-tauri/src/commands.rs
use tauri::State;
use crate::node::NodeManager;

#[tauri::command]
pub async fn get_node_status(
    manager: State<'_, NodeManager>,
) -> Result<NodeStatus, String> {
    manager.status().await.map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn start_node(
    manager: State<'_, NodeManager>,
) -> Result<(), String> {
    manager.start().await.map_err(|e| e.to_string())
}
Register in
main.rs
:
rust
// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .manage(NodeManager::new())
        .invoke_handler(tauri::generate_handler![
            commands::get_node_status,
            commands::start_node,
            commands::stop_node,
            commands::get_config,
            commands::save_config,
            commands::run_preflight,
            commands::get_gpu_devices,
            commands::check_for_updates,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
rust
// src-tauri/src/commands.rs
use tauri::State;
use crate::node::NodeManager;

#[tauri::command]
pub async fn get_node_status(
    manager: State<'_, NodeManager>,
) -> Result<NodeStatus, String> {
    manager.status().await.map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn start_node(
    manager: State<'_, NodeManager>,
) -> Result<(), String> {
    manager.start().await.map_err(|e| e.to_string())
}
main.rs
中注册:
rust
// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .manage(NodeManager::new())
        .invoke_handler(tauri::generate_handler![
            commands::get_node_status,
            commands::start_node,
            commands::stop_node,
            commands::get_config,
            commands::save_config,
            commands::run_preflight,
            commands::get_gpu_devices,
            commands::check_for_updates,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Emitting Live Log Events to Frontend

向前端发送实时日志事件

rust
// src-tauri/src/node.rs
use tauri::{AppHandle, Emitter};

pub async fn stream_logs(app: AppHandle, mut reader: impl AsyncBufRead + Unpin) {
    let mut line = String::new();
    loop {
        line.clear();
        match reader.read_line(&mut line).await {
            Ok(0) => break, // EOF
            Ok(_) => {
                app.emit("node-log-line", line.trim().to_string())
                   .unwrap_or_default();
            }
            Err(e) => {
                app.emit("node-log-error", e.to_string()).unwrap_or_default();
                break;
            }
        }
    }
}
Listen in the frontend:
js
const { listen } = window.__TAURI__.event;

const unlisten = await listen('node-log-line', (event) => {
    appendToLogDrawer(event.payload);
});

// Cleanup when drawer closes:
unlisten();
rust
// src-tauri/src/node.rs
use tauri::{AppHandle, Emitter};

pub async fn stream_logs(app: AppHandle, mut reader: impl AsyncBufRead + Unpin) {
    let mut line = String::new();
    loop {
        line.clear();
        match reader.read_line(&mut line).await {
            Ok(0) => break, // 文件结束
            Ok(_) => {
                app.emit("node-log-line", line.trim().to_string())
                   .unwrap_or_default();
            }
            Err(e) => {
                app.emit("node-log-error", e.to_string()).unwrap_or_default();
                break;
            }
        }
    }
}
在前端监听:
js
const { listen } = window.__TAURI__.event;

const unlisten = await listen('node-log-line', (event) => {
    appendToLogDrawer(event.payload);
});

// 关闭日志抽屉时清理:
unlisten();

Docker Mode: Managing Containers

Docker模式:容器管理

rust
// src-tauri/src/docker.rs
use std::process::Command;

pub fn pull_image(image: &str) -> Result<(), String> {
    let status = Command::new("docker")
        .args(["pull", image])
        .status()
        .map_err(|e| format!("docker not found: {e}"))?;
    if status.success() { Ok(()) } else { Err("docker pull failed".into()) }
}

pub fn run_node_container(image: &str, data_dir: &str, port: u16) -> Result<String, String> {
    let output = Command::new("docker")
        .args([
            "run", "-d",
            "--name", "quip-node",
            "-p", &format!("{port}:{port}"),
            "-v", &format!("{data_dir}:/quip-data"),
            image,
        ])
        .output()
        .map_err(|e| e.to_string())?;
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}
rust
// src-tauri/src/docker.rs
use std::process::Command;

pub fn pull_image(image: &str) -> Result<(), String> {
    let status = Command::new("docker")
        .args(["pull", image])
        .status()
        .map_err(|e| format!("docker not found: {e}"))?;
    if status.success() { Ok(()) } else { Err("docker pull failed".into()) }
}

pub fn run_node_container(image: &str, data_dir: &str, port: u16) -> Result<String, String> {
    let output = Command::new("docker")
        .args([
            "run", "-d",
            "--name", "quip-node",
            "-p", &format!("{port}:{port}"),
            "-v", &format!("{data_dir}:/quip-data"),
            image,
        ])
        .output()
        .map_err(|e| e.to_string())?;
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}

GPU Detection

GPU检测

rust
// src-tauri/src/gpu.rs
#[derive(serde::Serialize)]
pub struct GpuDevice {
    pub id: String,
    pub name: String,
    pub device_type: String, // "cuda" or "metal"
    pub enabled: bool,
    pub utilization: u8,
}

pub fn detect_gpus() -> Vec<GpuDevice> {
    let mut devices = Vec::new();

    // CUDA detection via nvidia-smi
    if let Ok(output) = std::process::Command::new("nvidia-smi")
        .args(["--query-gpu=index,name", "--format=csv,noheader"])
        .output()
    {
        for line in String::from_utf8_lossy(&output.stdout).lines() {
            let parts: Vec<&str> = line.splitn(2, ',').collect();
            if parts.len() == 2 {
                devices.push(GpuDevice {
                    id: parts[0].trim().to_string(),
                    name: parts[1].trim().to_string(),
                    device_type: "cuda".into(),
                    enabled: true,
                    utilization: 80,
                });
            }
        }
    }

    // macOS Metal: presence of Metal framework implies GPU
    #[cfg(target_os = "macos")]
    devices.push(GpuDevice {
        id: "0".into(),
        name: "Apple Metal GPU".into(),
        device_type: "metal".into(),
        enabled: true,
        utilization: 80,
    });

    devices
}

rust
// src-tauri/src/gpu.rs
#[derive(serde::Serialize)]
pub struct GpuDevice {
    pub id: String,
    pub name: String,
    pub device_type: String, // "cuda" 或 "metal"
    pub enabled: bool,
    pub utilization: u8,
}

pub fn detect_gpus() -> Vec<GpuDevice> {
    let mut devices = Vec::new();

    // 通过nvidia-smi检测CUDA
    if let Ok(output) = std::process::Command::new("nvidia-smi")
        .args(["--query-gpu=index,name", "--format=csv,noheader"])
        .output()
    {
        for line in String::from_utf8_lossy(&output.stdout).lines() {
            let parts: Vec<&str> = line.splitn(2, ',').collect();
            if parts.len() == 2 {
                devices.push(GpuDevice {
                    id: parts[0].trim().to_string(),
                    name: parts[1].trim().to_string(),
                    device_type: "cuda".into(),
                    enabled: true,
                    utilization: 80,
                });
            }
        }
    }

    // macOS Metal:存在Metal框架即表示有GPU
    #[cfg(target_os = "macos")]
    devices.push(GpuDevice {
        id: "0".into(),
        name: "Apple Metal GPU".into(),
        device_type: "metal".into(),
        enabled: true,
        utilization: 80,
    });

    devices
}

CLI Mode

CLI模式

sh
quip-node-manager --cli
Launches a terminal UI (TUI) instead of the desktop window — useful for headless servers.

sh
quip-node-manager --cli
启动终端UI(TUI)而非桌面窗口——适用于无界面服务器。

TLS Certificate Setup

TLS证书设置

The GUI includes a built-in walkthrough. For scripted setup:
Let's Encrypt (certbot):
sh
sudo certbot certonly --standalone -d your.domain.com
GUI内置了设置向导。如需脚本化设置:
Let's Encrypt(certbot):
sh
sudo certbot certonly --standalone -d your.domain.com

Certs written to /etc/letsencrypt/live/your.domain.com/

证书会写入/etc/letsencrypt/live/your.domain.com/


Then set in config:
```toml
[network]
tls_cert = "/etc/letsencrypt/live/your.domain.com/fullchain.pem"
tls_key  = "/etc/letsencrypt/live/your.domain.com/privkey.pem"
Self-signed (openssl):
sh
openssl req -x509 -newkey rsa:4096 -keyout ~/quip-data/key.pem \
  -out ~/quip-data/cert.pem -days 365 -nodes \
  -subj "/CN=quip-node"


然后在配置中设置:
```toml
[network]
tls_cert = "/etc/letsencrypt/live/your.domain.com/fullchain.pem"
tls_key  = "/etc/letsencrypt/live/your.domain.com/privkey.pem"
自签名证书(openssl):
sh
openssl req -x509 -newkey rsa:4096 -keyout ~/quip-data/key.pem \
  -out ~/quip-data/cert.pem -days 365 -nodes \
  -subj "/CN=quip-node"

Environment Variables

环境变量

VariablePurpose
DWAVE_TOKEN
D-Wave QPU API token (never hard-code)
QUIP_DATA_DIR
Override default
~/quip-data/
location
QUIP_NODE_IMAGE
Override Docker image tag
RUST_LOG
Logging level (e.g.
debug
,
info
)

环境变量用途
DWAVE_TOKEN
D-Wave QPU API令牌(切勿硬编码)
QUIP_DATA_DIR
覆盖默认的
~/quip-data/
目录位置
QUIP_NODE_IMAGE
覆盖Docker镜像标签
RUST_LOG
日志级别(例如
debug
info

Background Update Monitor

后台更新监控

The app checks every 30 minutes for:
  • New Docker images
  • New native binaries
  • New app releases
Configure in
config.toml
:
toml
[updates]
check_interval_secs = 1800
auto_restart_on_image_update = true  # restarts node on new Docker image

应用每30分钟检查一次:
  • 新的Docker镜像
  • 新的原生二进制文件
  • 新的应用版本
可在
config.toml
中配置:
toml
[updates]
check_interval_secs = 1800
auto_restart_on_image_update = true  # 有新Docker镜像时自动重启节点

Troubleshooting

故障排查

ProblemFix
macOS "app is damaged"Run
xattr -dr com.apple.quarantine "/Applications/Quip Node Manager.app"
Windows SmartScreen blockClick More info → Run anyway
Docker not foundInstall Docker Desktop and ensure daemon is running
Pre-flight: port closedForward UDP/TCP port 9000 on router; check
ufw
/
iptables
Node won't start (native)Check
~/quip-data/
for secret file; re-run preflight
Blank GUI window (dev)Run
bun install
then
bun run dev
; check Tauri prerequisites
cargo clippy
errors
Run
rustup update stable
to ensure latest stable Rust
Logs not streamingEnsure
node-log-line
event listener registered before
start_node
invoke

问题解决方法
macOS提示“应用已损坏”运行
xattr -dr com.apple.quarantine "/Applications/Quip Node Manager.app"
Windows SmartScreen拦截点击更多信息 → 仍要运行
找不到Docker安装Docker Desktop并确保守护进程正在运行
预启动检查:端口关闭在路由器上转发UDP/TCP 9000端口;检查
ufw
/
iptables
设置
节点无法启动(Native模式)检查
~/quip-data/
中是否存在密钥文件;重新运行预启动检查
GUI窗口空白(开发环境)运行
bun install
后再执行
bun run dev
;检查Tauri前置条件是否满足
cargo clippy
报错
运行
rustup update stable
确保使用最新稳定版Rust
日志无法流式传输确保在调用
start_node
前已注册
node-log-line
事件监听器

Contributing

贡献指南

  1. Fork on GitLab:
    https://gitlab.com/quip.network/quip-node-manager
  2. Make changes; run
    cargo clippy
    and
    bun run build
    before submitting
  3. License: AGPL-3.0-or-later — all contributions must be compatible
  4. See AGENTS.md for AI-agent-specific architecture guidance
  1. 在GitLab上复刻项目:
    https://gitlab.com/quip.network/quip-node-manager
  2. 进行修改;提交前需运行
    cargo clippy
    bun run build
  3. 许可证:AGPL-3.0-or-later —— 所有贡献必须符合该许可证要求
  4. 查看AGENTS.md获取AI Agent相关的架构指导