feat(viking): add local server management for privacy-first deployment

- Add viking_server.rs (Rust) for managing local OpenViking server process
- Add viking-server-manager.ts (TypeScript) for server control from UI
- Update VikingAdapter to support 'local' mode with auto-start capability
- Update documentation for local deployment mode

Key features:
- Auto-start local server when needed
- All data stays in ~/.openviking/ (privacy-first)
- Server listens only on 127.0.0.1
- Graceful fallback to remote/localStorage modes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-16 08:14:44 +08:00
parent 137f1a32fa
commit c8202d04e0
6 changed files with 1721 additions and 1 deletions

View File

@@ -22,4 +22,9 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "blocking"] }
chrono = "0.4"
regex = "1"
dirs = "5"

View File

@@ -3,6 +3,15 @@
// - Port: 4200 (was 18789)
// - Binary: openfang (was openclaw)
// - Config: ~/.openfang/openfang.toml (was ~/.openclaw/openclaw.json)
// Viking CLI sidecar module for local memory operations
mod viking_commands;
mod viking_server;
// Memory extraction and context building modules (supplement CLI)
mod memory;
mod llm;
use serde::Serialize;
use serde_json::{json, Value};
use std::fs;
@@ -1006,7 +1015,27 @@ pub fn run() {
gateway_local_auth,
gateway_prepare_for_tauri,
gateway_approve_device_pairing,
gateway_doctor
gateway_doctor,
// OpenViking CLI sidecar commands
viking_commands::viking_status,
viking_commands::viking_add,
viking_commands::viking_add_inline,
viking_commands::viking_find,
viking_commands::viking_grep,
viking_commands::viking_ls,
viking_commands::viking_read,
viking_commands::viking_remove,
viking_commands::viking_tree,
// Viking server management (local deployment)
viking_server::viking_server_status,
viking_server::viking_server_start,
viking_server::viking_server_stop,
viking_server::viking_server_restart,
// Memory extraction commands (supplement CLI)
memory::extractor::extract_session_memories,
memory::context_builder::estimate_content_tokens,
// LLM commands (for extraction)
llm::llm_complete
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -0,0 +1,295 @@
//! OpenViking Local Server Management
//!
//! Manages a local OpenViking server instance for privacy-first deployment.
//! All data is stored locally in ~/.openviking/ - nothing is uploaded to remote servers.
//!
//! Architecture:
//! ┌─────────────────────────────────────────────────────────────────┐
//! │ ZCLAW Desktop (Tauri) │
//! │ │
//! │ ┌─────────────────┐ HTTP ┌─────────────────────────┐ │
//! │ │ viking_commands │ ◄────────────►│ openviking-server │ │
//! │ │ (Tauri cmds) │ localhost │ (Python, managed here) │ │
//! │ └─────────────────┘ └───────────┬─────────────┘ │
//! │ │ │
//! │ ┌─────────▼─────────────┐ │
//! │ │ SQLite + Vector Store │ │
//! │ │ ~/.openviking/ │ │
//! │ │ (LOCAL DATA ONLY) │ │
//! │ └───────────────────────┘ │
//! └─────────────────────────────────────────────────────────────────┘
use serde::{Deserialize, Serialize};
use std::process::{Child, Command};
use std::sync::Mutex;
use std::time::Duration;
// === Types ===
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerStatus {
pub running: bool,
pub port: u16,
pub pid: Option<u32>,
pub data_dir: Option<String>,
pub version: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerConfig {
pub port: u16,
pub data_dir: String,
pub config_file: Option<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
let home = dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
Self {
port: 1933,
data_dir: format!("{}/.openviking/workspace", home),
config_file: Some(format!("{}/.openviking/ov.conf", home)),
}
}
}
// === Server Process Management ===
static SERVER_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
/// Check if OpenViking server is running
fn is_server_running(port: u16) -> bool {
// Try to connect to the server
let url = format!("http://127.0.0.1:{}/api/v1/status", port);
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.ok();
if let Some(client) = client {
if let Ok(resp) = client.get(&url).send() {
return resp.status().is_success();
}
}
false
}
/// Find openviking-server executable
fn find_server_binary() -> Result<String, String> {
// Check environment variable first
if let Ok(path) = std::env::var("ZCLAW_VIKING_SERVER_BIN") {
if std::path::Path::new(&path).exists() {
return Ok(path);
}
}
// Check common locations
let candidates = vec![
"openviking-server".to_string(),
"python -m openviking.server".to_string(),
];
// Try to find in PATH
for cmd in &candidates {
if Command::new("which")
.arg(cmd.split_whitespace().next().unwrap_or(""))
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Ok(cmd.clone());
}
}
// Check Python virtual environment
let home = dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let venv_candidates = vec![
format!("{}/.openviking/venv/bin/openviking-server", home),
format!("{}/.local/bin/openviking-server", home),
];
for path in venv_candidates {
if std::path::Path::new(&path).exists() {
return Ok(path);
}
}
// Fallback: assume it's in PATH via pip install
Ok("openviking-server".to_string())
}
// === Tauri Commands ===
/// Get server status
#[tauri::command]
pub fn viking_server_status() -> Result<ServerStatus, String> {
let config = ServerConfig::default();
let running = is_server_running(config.port);
let pid = if running {
SERVER_PROCESS
.lock()
.map(|guard| guard.as_ref().map(|c| c.id()))
.ok()
.flatten()
} else {
None
};
// Get version if running
let version = if running {
let url = format!("http://127.0.0.1:{}/api/v1/version", config.port);
reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.ok()
.and_then(|client| client.get(&url).send().ok())
.and_then(|resp| resp.text().ok())
} else {
None
};
Ok(ServerStatus {
running,
port: config.port,
pid,
data_dir: Some(config.data_dir),
version,
error: None,
})
}
/// Start local OpenViking server
#[tauri::command]
pub fn viking_server_start(config: Option<ServerConfig>) -> Result<ServerStatus, String> {
let config = config.unwrap_or_default();
// Check if already running
if is_server_running(config.port) {
return Ok(ServerStatus {
running: true,
port: config.port,
pid: None,
data_dir: Some(config.data_dir),
version: None,
error: Some("Server already running".to_string()),
});
}
// Find server binary
let server_bin = find_server_binary()?;
// Ensure data directory exists
std::fs::create_dir_all(&config.data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
// Set environment variables
if let Some(ref config_file) = config.config_file {
std::env::set_var("OPENVIKING_CONFIG_FILE", config_file);
}
// Start server process
let child = if server_bin.contains("python") {
// Use Python module
let parts: Vec<&str> = server_bin.split_whitespace().collect();
Command::new(parts[0])
.args(&parts[1..])
.arg("--host")
.arg("127.0.0.1")
.arg("--port")
.arg(config.port.to_string())
.spawn()
.map_err(|e| format!("Failed to start server: {}", e))?
} else {
// Direct binary
Command::new(&server_bin)
.arg("--host")
.arg("127.0.0.1")
.arg("--port")
.arg(config.port.to_string())
.spawn()
.map_err(|e| format!("Failed to start server: {}", e))?
};
let pid = child.id();
// Store process handle
if let Ok(mut guard) = SERVER_PROCESS.lock() {
*guard = Some(child);
}
// Wait for server to be ready
let mut ready = false;
for _ in 0..30 {
std::thread::sleep(Duration::from_millis(500));
if is_server_running(config.port) {
ready = true;
break;
}
}
if !ready {
return Err("Server failed to start within 15 seconds".to_string());
}
Ok(ServerStatus {
running: true,
port: config.port,
pid: Some(pid),
data_dir: Some(config.data_dir),
version: None,
error: None,
})
}
/// Stop local OpenViking server
#[tauri::command]
pub fn viking_server_stop() -> Result<(), String> {
if let Ok(mut guard) = SERVER_PROCESS.lock() {
if let Some(mut child) = guard.take() {
child.kill().map_err(|e| format!("Failed to kill server: {}", e))?;
}
}
Ok(())
}
/// Restart local OpenViking server
#[tauri::command]
pub fn viking_server_restart(config: Option<ServerConfig>) -> Result<ServerStatus, String> {
viking_server_stop()?;
std::thread::sleep(Duration::from_secs(1));
viking_server_start(config)
}
// === Tests ===
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_config_default() {
let config = ServerConfig::default();
assert_eq!(config.port, 1933);
assert!(config.data_dir.contains(".openviking"));
}
#[test]
fn test_is_server_running_not_running() {
// Should return false when no server is running on port 1933
let result = is_server_running(1933);
// Just check it doesn't panic
assert!(result || !result);
}
}