Files
openhuman/src-tauri/src/services/tdlib_v8/ops/mod.rs
T
3fb5e1dd11 feat: local model support and SocketManager WebSocket transport (#36)
* chore: add simulation function for deep link testing in development

- Introduced a simulation function for deep links in the development environment to facilitate testing.
- The function allows developers to simulate deep link URLs without affecting production behavior.

* feat: implement QuickJS skill runtime integration

- Added a new QuickJS skill runtime engine to manage skill execution within the application.
- Introduced commands for skill management, including discovery, starting, stopping, and querying skill states.
- Implemented a SQLite database bridge for each skill to handle data storage.
- Enhanced the application structure with new modules for runtime management, skill instances, and manifest parsing.
- Updated dependencies in Cargo.toml and Cargo.lock to support new features.

* refactor: improve code readability and formatting in DownloadScreen and deviceDetection

- Enhanced the button className formatting in DownloadScreen for better readability.
- Reformatted the fetchLatestRelease and parseReleaseAssetsByArchitecture functions in deviceDetection for improved clarity and maintainability.
- Utilized multiline formatting for complex conditions to enhance code structure.

* feat: integrate Rust-native Socket.io client for persistent connections

- Implemented a SocketManager in Rust to handle Socket.io connections, ensuring persistence across app backgrounding.
- Updated SocketProvider to connect/disconnect using Rust-native methods in Tauri mode.
- Enhanced Tauri event listeners for socket state changes and server events.
- Refactored socket handling logic to differentiate between web and Tauri modes, improving maintainability and clarity.
- Added new commands for connecting, disconnecting, and emitting events through the Rust socket.

* refactor: remove Telegram login commands and related functionality

- Deleted the startTelegramLogin and startTelegramLoginWithUrl functions from the Tauri commands.
- Removed associated references in the Rust command module and utility file.
- This cleanup simplifies the codebase by eliminating unused Telegram login features.

* feat: add cron scheduling functionality for skills

- Introduced a new CronScheduler to manage scheduled tasks for skills, allowing for cron expression parsing and execution.
- Added a cron bridge to expose scheduling methods to skill contexts, enabling skills to register, unregister, and list cron schedules.
- Updated the RuntimeEngine to initialize and manage the cron scheduler, ensuring it runs in the background.
- Enhanced skill instances to support cron scheduling through a new BridgeDeps structure.
- Updated dependencies in Cargo.toml and Cargo.lock to include the cron crate.

* feat: add Android support and enhance dependencies

- Introduced Android-specific build and run commands in package.json for Tauri.
- Updated Cargo.toml to include OpenSSL for Android and adjusted reqwest for cross-platform TLS support.
- Added new mobile capabilities configuration for Android and iOS.
- Implemented a foreground service in the Android app to maintain the Rust backend process.
- Added various Android resources including layouts, icons, and notification handling.
- Updated Cargo.lock with new dependencies for improved functionality.

* chore: update .gitignore and AndroidManifest.xml for deep link support

- Added .kotlin and .cargo to .gitignore to exclude Kotlin and Cargo build artifacts.
- Updated AndroidManifest.xml with comments for the deep link plugin, clarifying its auto-generated nature.

* feat: enhance store configuration for development testing

- Added functionality to auto-inject a JWT token from environment variables during development.
- Implemented automatic onboarding for users once their profile is fetched, improving the testing experience.

* feat: enhance socket connection handling and add Android logging support

- Updated the Rust socket connection command to accept an optional URL parameter, allowing for dynamic backend URL configuration.
- Integrated Android logging capabilities using the android_logger crate, ensuring logs are properly routed to logcat.
- Improved error handling in the SocketManager to log connection errors and successful connections for better debugging.

* feat: implement notification permission handling and enhance foreground service in Android

- Added runtime permission request for POST_NOTIFICATIONS in MainActivity to comply with API 33+ requirements.
- Updated RuntimeService to specify foreground service type for API 34+ compatibility.
- Improved logging levels in the Rust backend for better debugging and monitoring of socket connections and skill discovery.

* feat: integrate QuickJS skill management and service

- Added QuickJS skill hooks for retrieving and managing QuickJS skills from Redux.
- Implemented a QuickJS service to handle skill lifecycle, preferences, and IPC calls with the Rust backend.
- Enhanced the skills state in Redux to include QuickJS skills, enabling better management and state tracking.
- Updated the store configuration to persist QuickJS skills state across sessions.
- Introduced new commands in the Rust backend for enabling/disabling skills and managing preferences.
- Improved the SkillProvider to initialize the QuickJS service during app startup.

* refactor: transition skill management to QuickJS runtime

- Removed legacy skill catalog and loading logic, replacing it with QuickJS runtime integration for skill discovery and management.
- Updated SkillProvider to utilize QuickJS for skill registration and lifecycle management.
- Simplified skill data handling by directly invoking the Rust backend for skill operations.
- Enhanced error handling and logging for skill loading processes.
- Cleaned up unused interfaces and functions related to previous skill management methods.

* refactor: remove unused skill commands and plugins

- Deleted the skills command module and related functions to streamline the codebase.
- Removed dependencies on the tauri-plugin-shell and other unused plugins from Cargo.toml and Cargo.lock.
- Updated the authentication module by removing the exchange_token function and its associated structures.
- Simplified the capabilities configuration by eliminating shell-related permissions.
- Enhanced the runtime engine to support new JSON-RPC commands for skill data management.

* feat: expose whitelisted environment values to skills

- Added `platform.env(key)` function to retrieve whitelisted environment values for skills.
- Implemented `get_skill_env` function to provide access to `BACKEND_URL` and `PLATFORM`.
- Updated `get_backend_url` to check for `VITE_BACKEND_URL` before falling back to `BACKEND_URL`.
- Enhanced the QuickJS runtime to support the new environment functionality.

* feat: fix skills enable/disable flow, setup pipeline, and status derivation

- Add SkillSetup struct to Rust manifest and include setup field in
  discovery response so the frontend knows which skills need setup
- Map setup field in SkillProvider discoverSkills()
- Fix SkillsGrid to use real hasSetup from manifest instead of hardcoding false
- Add contextual Enable/Setup/Configure/Retry buttons in management modal
- Add status indicator dots to compact skill table rows
- Fix deriveConnectionStatus to return "connected" for ready skills with
  completed setup that don't push host state (e.g. cron-based skills)
- Add select option renderer in SkillManagementPanel for non-boolean options
- Add dotenvy crate to load .env file at Rust startup so env vars like
  VITE_BACKEND_URL are available to the runtime engine and skills

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add platform support for skills and enhance manifest handling

- Introduced platform filtering for skills in the SkillManifest, allowing skills to specify supported platforms.
- Updated QuickJSManifest and runtime engine to handle platform checks, ensuring skills are only loaded on compatible platforms.
- Added new build and watch commands for skills in package.json to streamline development.
- Enhanced the Rust backend to log unsupported skills based on platform restrictions.

* feat: update skills submodule with TypeScript pipeline and test harness

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: transition from QuickJS to V8 runtime for skill management

- Replaced QuickJS with V8 (via deno_core) for improved JavaScript execution and WASM support.
- Updated skill management logic to utilize the new V8 runtime, including changes to the RuntimeEngine and skill instance handling.
- Enhanced manifest handling to support the new runtime and added platform compatibility checks.
- Introduced new dependencies and updated Cargo.toml to reflect changes in the skill execution environment.
- Refactored related modules and commands to align with the V8 integration, ensuring a seamless transition for skill operations.

* refactor: clean up dead code in IdbStorage and enhance ops module

- Removed unused `#[allow(dead_code)]` annotations from the IdbStorage struct and its methods to improve code clarity.
- Introduced new timer and WebSocket state management structures in the ops module, laying groundwork for future enhancements.
- Added timer and WebSocket operation functions, including `op_timer_start`, `op_timer_cancel`, and WebSocket connection handling, to support asynchronous operations.

* feat: implement high-level TDLib service with V8 integration

- Introduced a new `TdlibV8Service` to manage TDLib client instances using the V8 runtime.
- Added a `MockTdClient` for development and testing, simulating TDLib responses for various queries.
- Enhanced the bootstrap script to initialize and manage TDLib clients, including methods for sending queries and retrieving authentication states.
- Updated the `mod.rs` file to include the new service module and improved documentation for clarity.
- Refactored existing code to support the new TDLib integration, ensuring a seamless experience for skill management.

* refactor: update V8 runtime integration and platform handling

- Updated the V8 runtime integration to ensure it is only available on desktop platforms, with appropriate error handling for mobile.
- Refactored the `SocketManager` and command implementations to conditionally include desktop-only features, enhancing clarity and maintainability.
- Cleaned up the `Cargo.toml` to reflect the changes in V8 runtime availability and added relevant documentation.
- Removed dead code related to mobile platform handling in the TDLib integration, ensuring a streamlined codebase.

* chore: update skills submodule to latest commit

- Updated the skills submodule to the latest commit (19a18e8), ensuring alignment with recent changes and improvements in the codebase.

* refactor: transition from QuickJS to V8 runtime for skill management

- Updated the skills submodule to the latest commit, reflecting the transition from QuickJS to V8 for improved JavaScript execution.
- Refactored skill management logic to utilize the V8 runtime, including changes to the RuntimeEngine and skill instance handling.
- Enhanced manifest handling to support the new runtime and updated platform compatibility checks.
- Removed dead code related to QuickJS, ensuring a streamlined codebase.
- Updated comments and documentation to reflect the changes in runtime integration.

* feat: enhance logging and update timer operations in V8 integration

- Added logging for skill discovery and manifest processing in the V8 runtime, improving traceability during skill management.
- Updated timer operation functions to use new prefixed names (`op_ah_timer_start` and `op_ah_timer_cancel`) to avoid conflicts with deno_core built-ins.
- Implemented a mechanism to load .env files from various locations, ensuring environment variables are available for configuration.
- Enhanced the `get_backend_url` function to include debug logging for better visibility of the backend URL resolution process.

* chore: update skills submodule to latest commit and enhance logging

- Updated the skills submodule to the latest commit (54c40a1), ensuring alignment with recent changes.
- Added console logging for skill manifests during discovery in the SkillsGrid component to improve debugging and traceability.

* refactor: clean up and format code in DownloadScreen and SkillsGrid components

- Improved code readability by formatting multi-line statements in the DownloadScreen and SkillsGrid components.
- Removed unnecessary type imports in DownloadScreen for clarity.
- Enhanced the structure of the SkillsGrid component by adjusting the layout of JSX elements for better maintainability.
- Updated socketService to ensure consistent import order and improved logging in TauriSocket utility functions.

* chore: update development script in package.json

- Replaced the existing setup-python-sidecar script with a new dev:app script to streamline the development process for Tauri applications, enabling better debugging with RUST_BACKTRACE and RUST_LOG settings.

* refactor: update load method to accept additional parameters

- Modified the load method in SkillRuntime to accept an optional additionalParams argument, enhancing flexibility for skill loading.
- Ensured compatibility by using a fallback to an empty object when additionalParams is not provided.

* refactor: improve JSX structure in SkillsGrid component

- Enhanced the formatting of the connection status indicator in the SkillsGrid component for better readability.
- Adjusted the indentation and spacing to maintain consistent code style and improve maintainability.

* Refactor invoke_handler to consolidate command registration across platforms

- Simplified the command registration process by merging desktop and mobile command handlers into a single `invoke_handler` call.
- Improved code readability and maintainability by reducing duplication in command definitions.
- Ensured that window commands remain desktop-only while maintaining common functionality for all platforms.

* updated icon

* Update skills submodule to latest commit and remove redundant logging statements in V8 engine skill discovery

* Update skills submodule to reflect dirty state

* feat: expose BACKEND_URL environment variable for skills

- Added "BACKEND_URL" to the list of whitelisted environment variables accessible to skills, allowing for broader usage without the "VITE_" prefix.

* Enhance V8 runtime with async event loop and timer management

- Implemented an async event loop in the V8 skill instance to handle timers and messages efficiently.
- Introduced a new TimerState structure for managing scheduled timers, allowing for better integration with the V8 event loop.
- Updated the dotenv loading mechanism to check for environment variables in the current and parent directories.
- Enhanced logging for timer operations and skill management processes to improve traceability.

* Update skills submodule to latest commit and expose additional environment variables for skills

- Updated the skills submodule to commit 3793fdc, ensuring alignment with recent changes.
- Added "TELEGRAM_API_ID" and "TELEGRAM_API_HASH" to the list of whitelisted environment variables, allowing skills to access these without the "VITE_" prefix.

* Integrate TDLib support for Telegram skill

- Added TDLib commands for creating, sending, receiving, and destroying clients, enabling Telegram functionality.
- Implemented a TdLibManager for managing TDLib client lifecycle and asynchronous operations on desktop platforms.
- Introduced JNI bridge for Android to facilitate TDLib interactions.
- Updated Cargo.toml and Cargo.lock to include tdlib-rs and related dependencies.
- Enhanced the V8 runtime to support TDLib operations, ensuring compatibility across platforms.

* Enhance TDLib manager with update queue and async polling

- Introduced an update queue and notification channel in ClientState for managing TDLib updates.
- Implemented a separate polling task for TDLib updates to improve responsiveness and prevent blocking the main event loop.
- Updated lifecycle function handling in the V8 runtime to avoid waiting for the event loop, allowing for long-running async operations.
- Improved error handling and logging for better traceability during TDLib operations.

* Enhance documentation and improve code formatting

- Updated CLAUDE.md to include new core commands and runtime management features for the Rust backend, as well as Android support and platform-specific details.
- Refactored SettingsHeader component in SettingsHeader.tsx for improved readability by consolidating props and cleaning up JSX formatting.
- Enhanced SkillProvider.tsx with better logging for skill loading errors, improving traceability in the development process.

* Integrate local LLM inference support and update dependencies

- Added local LLM inference capabilities for desktop and Android platforms, excluding iOS.
- Introduced new model commands for checking availability, generating text, and summarizing content.
- Updated Cargo.toml and Cargo.lock to include necessary dependencies such as `llama-cpp-2` and `encoding_rs`.
- Enhanced the V8 runtime and JavaScript API to support model operations, improving the overall functionality of the application.

* Enhance SocketManager with WebSocket transport type

- Added support for WebSocket transport type in SocketManager for improved connection handling.
- Updated connection configuration to utilize WebSocketUpgrade, enhancing real-time communication capabilities.

* style: fix Prettier formatting in ModelDownloadProgress

* chore: update skills submodule to latest commit

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 08:15:00 +05:30

1237 lines
36 KiB
Rust

//! Custom Deno Core Ops for V8 Runtime
//!
//! Provides browser API implementations as Rust ops.
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use deno_core::{extension, op2, OpState};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use super::storage::IdbStorage;
// ============================================================================
// Extension Definition
// ============================================================================
extension!(
alphahuman_ops,
ops = [
// Console ops
op_console_log,
op_console_warn,
op_console_error,
// Crypto ops
op_crypto_random,
op_atob,
op_btoa,
// Performance ops
op_performance_now,
// Platform ops
op_platform_os,
op_platform_env,
// Timer ops (prefixed to avoid conflict with deno_core built-ins)
op_ah_timer_start,
op_ah_timer_cancel,
// Fetch ops
op_fetch,
// WebSocket ops
op_ws_connect,
op_ws_send,
op_ws_recv,
op_ws_close,
// IndexedDB ops
op_idb_open,
op_idb_close,
op_idb_delete_database,
op_idb_create_object_store,
op_idb_delete_object_store,
op_idb_get,
op_idb_put,
op_idb_delete,
op_idb_clear,
op_idb_get_all,
op_idb_get_all_keys,
op_idb_count,
// Skill bridge ops
op_db_exec,
op_db_get,
op_db_all,
op_db_kv_get,
op_db_kv_set,
op_store_get,
op_store_set,
op_store_delete,
op_store_keys,
op_net_fetch,
// State bridge ops
op_state_get,
op_state_set,
op_state_set_partial,
// Data bridge ops
op_data_read,
op_data_write,
// TDLib ops (telegram skill only)
op_tdlib_create_client,
op_tdlib_send,
op_tdlib_receive,
op_tdlib_destroy,
op_tdlib_is_available,
// Model ops (local LLM inference)
op_model_is_available,
op_model_get_status,
op_model_generate,
op_model_summarize,
],
state = |state| {
// State will be initialized when runtime is created
}
);
/// Build the deno_core Extension with all custom ops.
pub fn build_extension(_storage: IdbStorage) -> deno_core::Extension {
alphahuman_ops::init_ops_and_esm()
}
/// Initialize storage in op state with data directory and shared skill state.
pub fn init_state_with_data_dir(
state: &mut OpState,
storage: IdbStorage,
skill_id: String,
data_dir: std::path::PathBuf,
skill_state: std::sync::Arc<parking_lot::RwLock<crate::runtime::v8_skill_instance::SkillState>>,
) {
state.put(storage);
state.put(SkillContext::with_state(skill_id, data_dir, skill_state));
state.put(TimerState::default());
state.put(WebSocketState::default());
}
/// Poll timers and return IDs of timers that are ready to fire.
/// Also returns the duration until the next timer (for efficient sleeping).
pub fn poll_timers(state: &mut OpState) -> (Vec<u32>, Option<std::time::Duration>) {
let timer_state = state.borrow_mut::<TimerState>();
let ready = timer_state.poll_ready();
let next = timer_state.time_until_next();
(ready, next)
}
/// Context for the current skill execution.
#[derive(Clone)]
pub struct SkillContext {
pub skill_id: String,
/// Skill-specific data directory for file I/O.
pub data_dir: Option<std::path::PathBuf>,
/// Shared reference to the skill's state (same instance as V8SkillInstance).
pub skill_state: Option<std::sync::Arc<parking_lot::RwLock<crate::runtime::v8_skill_instance::SkillState>>>,
}
impl Default for SkillContext {
fn default() -> Self {
Self {
skill_id: String::new(),
data_dir: None,
skill_state: None,
}
}
}
impl SkillContext {
/// Create a new SkillContext with a skill ID, data directory, and shared state.
pub fn with_state(
skill_id: String,
data_dir: std::path::PathBuf,
skill_state: std::sync::Arc<parking_lot::RwLock<crate::runtime::v8_skill_instance::SkillState>>,
) -> Self {
Self {
skill_id,
data_dir: Some(data_dir),
skill_state: Some(skill_state),
}
}
}
// ============================================================================
// Timer State
// ============================================================================
/// A scheduled timer (setTimeout or setInterval).
#[derive(Clone, Debug)]
pub struct TimerEntry {
/// Whether this is a repeating interval
pub is_interval: bool,
/// Delay in milliseconds
pub delay_ms: u64,
/// When the timer should fire next (Instant)
pub next_fire: std::time::Instant,
}
/// State for managing timers.
/// Timers are polled during the event loop and fire callbacks via JS.
#[derive(Default)]
pub struct TimerState {
/// Active timers: id -> TimerEntry
pub timers: HashMap<u32, TimerEntry>,
}
impl TimerState {
/// Get all timers that are ready to fire, returning their IDs.
/// For intervals, reschedules the next fire time.
/// For timeouts, removes them from the map.
pub fn poll_ready(&mut self) -> Vec<u32> {
let now = std::time::Instant::now();
let mut ready = Vec::new();
// Collect IDs of ready timers
let ready_ids: Vec<u32> = self
.timers
.iter()
.filter(|(_, entry)| now >= entry.next_fire)
.map(|(id, _)| *id)
.collect();
for id in ready_ids {
if let Some(entry) = self.timers.get_mut(&id) {
ready.push(id);
if entry.is_interval {
// Reschedule for next interval
entry.next_fire = now + std::time::Duration::from_millis(entry.delay_ms);
} else {
// Remove one-shot timeout
self.timers.remove(&id);
}
}
}
ready
}
/// Get the duration until the next timer fires (for sleep optimization).
pub fn time_until_next(&self) -> Option<std::time::Duration> {
let now = std::time::Instant::now();
self.timers
.values()
.map(|entry| {
if entry.next_fire > now {
entry.next_fire - now
} else {
std::time::Duration::ZERO
}
})
.min()
}
}
// ============================================================================
// WebSocket State
// ============================================================================
/// State for managing WebSocket connections.
/// Currently a placeholder - full state management is TODO.
#[derive(Default)]
#[allow(dead_code)]
pub struct WebSocketState {
next_id: u32,
/// Active connections: id -> WebSocket sender
pub connections: HashMap<u32, WebSocketConnection>,
}
#[allow(dead_code)]
pub struct WebSocketConnection {
pub write_tx: mpsc::Sender<String>,
pub read_rx: mpsc::Receiver<String>,
pub close_tx: Option<tokio::sync::oneshot::Sender<()>>,
}
// ============================================================================
// Console Ops
// ============================================================================
#[op2(fast)]
fn op_console_log(#[string] msg: &str) {
log::info!("[js] {}", msg);
}
#[op2(fast)]
fn op_console_warn(#[string] msg: &str) {
log::warn!("[js] {}", msg);
}
#[op2(fast)]
fn op_console_error(#[string] msg: &str) {
log::error!("[js] {}", msg);
}
// ============================================================================
// Crypto Ops
// ============================================================================
#[op2]
#[buffer]
fn op_crypto_random(len: u32) -> Vec<u8> {
use rand::RngCore;
let mut bytes = vec![0u8; len as usize];
rand::thread_rng().fill_bytes(&mut bytes);
bytes
}
#[op2]
#[string]
fn op_atob(#[string] input: &str) -> Result<String, deno_core::error::AnyError> {
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD.decode(input)?;
Ok(String::from_utf8_lossy(&decoded).to_string())
}
#[op2]
#[string]
fn op_btoa(#[string] input: &str) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(input.as_bytes())
}
// ============================================================================
// Performance Ops
// ============================================================================
#[op2(fast)]
fn op_performance_now() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64() * 1000.0)
.unwrap_or(0.0)
}
// ============================================================================
// Platform Ops
// ============================================================================
#[op2]
#[string]
fn op_platform_os() -> &'static str {
#[cfg(target_os = "windows")]
return "windows";
#[cfg(target_os = "macos")]
return "macos";
#[cfg(target_os = "linux")]
return "linux";
#[cfg(target_os = "android")]
return "android";
#[cfg(target_os = "ios")]
return "ios";
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "android",
target_os = "ios"
)))]
return "unknown";
}
#[op2]
#[string]
fn op_platform_env(#[string] key: &str) -> Option<String> {
const ALLOWED_ENV_VARS: &[&str] = &[
"VITE_TELEGRAM_BOT_USERNAME",
"VITE_TELEGRAM_BOT_ID",
"TELEGRAM_API_ID", // Skills may use this without VITE_ prefix
"TELEGRAM_API_HASH", // Skills may use this without VITE_ prefix
"VITE_BACKEND_URL",
"BACKEND_URL", // Skills may use this without VITE_ prefix
"VITE_DEBUG",
];
if ALLOWED_ENV_VARS.contains(&key) {
std::env::var(key).ok()
} else {
None
}
}
// ============================================================================
// Timer Ops
// ============================================================================
/// Start a timer (setTimeout or setInterval).
/// The actual callback execution happens in JavaScript via __handleTimer,
/// triggered by the event loop polling TimerState.
#[op2(fast)]
fn op_ah_timer_start(state: &mut OpState, id: u32, delay_ms: u32, is_interval: bool) {
let timer_state = state.borrow_mut::<TimerState>();
let entry = TimerEntry {
is_interval,
delay_ms: delay_ms as u64,
next_fire: std::time::Instant::now() + std::time::Duration::from_millis(delay_ms as u64),
};
timer_state.timers.insert(id, entry);
log::debug!(
"[timer] Registered {} {} with delay {}ms",
if is_interval { "interval" } else { "timeout" },
id,
delay_ms
);
}
/// Cancel a timer.
#[op2(fast)]
fn op_ah_timer_cancel(state: &mut OpState, id: u32) {
let timer_state = state.borrow_mut::<TimerState>();
if timer_state.timers.remove(&id).is_some() {
log::debug!("[timer] Cancelled timer {}", id);
}
}
// ============================================================================
// Fetch Ops
// ============================================================================
#[derive(Deserialize)]
struct FetchOptions {
method: Option<String>,
headers: Option<HashMap<String, String>>,
body: Option<String>,
}
#[derive(Serialize)]
struct FetchResponse {
status: u16,
#[serde(rename = "statusText")]
status_text: String,
headers: HashMap<String, String>,
body: String,
}
/// Async fetch operation for the fetch API.
#[op2(async)]
#[serde]
async fn op_fetch(
#[string] url: String,
#[serde] options: FetchOptions,
) -> Result<FetchResponse, deno_core::error::AnyError> {
let client = reqwest::Client::new();
let method = options.method.unwrap_or_else(|| "GET".to_string());
let method: reqwest::Method = method.parse().map_err(|_| {
deno_core::error::generic_error(format!("Invalid HTTP method: {}", method))
})?;
let mut request = client.request(method, &url);
// Add headers
if let Some(headers) = options.headers {
for (key, value) in headers {
request = request.header(&key, &value);
}
}
// Add body
if let Some(body) = options.body {
request = request.body(body);
}
let response = request.send().await.map_err(|e| {
deno_core::error::generic_error(format!("Fetch failed: {}", e))
})?;
let status = response.status().as_u16();
let status_text = response.status().canonical_reason().unwrap_or("").to_string();
let mut headers = HashMap::new();
for (key, value) in response.headers() {
if let Ok(v) = value.to_str() {
headers.insert(key.to_string(), v.to_string());
}
}
let body = response.text().await.map_err(|e| {
deno_core::error::generic_error(format!("Failed to read response body: {}", e))
})?;
Ok(FetchResponse {
status,
status_text,
headers,
body,
})
}
// ============================================================================
// WebSocket Ops
// ============================================================================
/// Connect to a WebSocket server.
#[op2(async)]
async fn op_ws_connect(
#[string] url: String,
) -> Result<u32, deno_core::error::AnyError> {
use futures::StreamExt;
use tokio_tungstenite::connect_async;
let (ws_stream, _) = connect_async(&url).await.map_err(|e| {
deno_core::error::generic_error(format!("WebSocket connect failed: {}", e))
})?;
let (write, mut read) = ws_stream.split();
// Create channels for communication
// Note: These are set up for future full WebSocket state management
let (_write_tx, write_rx) = mpsc::channel::<String>(32);
let (read_tx, _read_rx) = mpsc::channel::<String>(32);
let (_close_tx, close_rx) = tokio::sync::oneshot::channel::<()>();
// Spawn write task
let _write_handle = tokio::spawn(async move {
use futures::SinkExt;
use tokio_tungstenite::tungstenite::Message;
let mut write = write;
let mut write_rx_inner = write_rx;
let mut close_rx = close_rx;
loop {
tokio::select! {
msg = write_rx_inner.recv() => {
match msg {
Some(text) => {
if write.send(Message::Text(text)).await.is_err() {
break;
}
}
None => break,
}
}
_ = &mut close_rx => {
let _ = write.close().await;
break;
}
}
}
});
// Spawn read task
let _read_handle = tokio::spawn(async move {
use tokio_tungstenite::tungstenite::Message;
while let Some(result) = read.next().await {
match result {
Ok(Message::Text(text)) => {
if read_tx.send(text).await.is_err() {
break;
}
}
Ok(Message::Binary(data)) => {
// Convert binary to base64 for JavaScript
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
if read_tx.send(b64).await.is_err() {
break;
}
}
Ok(Message::Close(_)) => break,
Err(_) => break,
_ => {} // Ignore ping/pong
}
}
});
// Generate a simple ID (in production, use proper state management)
let id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u32)
.unwrap_or(1);
log::info!("[ws] Connected to {} with id {}", url, id);
Ok(id)
}
/// Send a message over WebSocket.
#[op2(fast)]
fn op_ws_send(
_state: &mut OpState,
_id: u32,
#[string] _data: &str,
) -> Result<(), deno_core::error::AnyError> {
// Note: Full WebSocket state management would require more complex handling
// For now, this is a placeholder
log::debug!("[ws] Send message on connection {}", _id);
Ok(())
}
/// Receive a message from WebSocket (async).
#[op2(async)]
#[string]
async fn op_ws_recv(_id: u32) -> Result<Option<String>, deno_core::error::AnyError> {
// Note: Full WebSocket state management would require more complex handling
// For now, return None to indicate no message
Ok(None)
}
/// Close a WebSocket connection.
#[op2(fast)]
fn op_ws_close(
_state: &mut OpState,
_id: u32,
_code: u16,
#[string] _reason: &str,
) -> Result<(), deno_core::error::AnyError> {
log::debug!("[ws] Close connection {}", _id);
Ok(())
}
// ============================================================================
// IndexedDB Ops
// ============================================================================
#[derive(Serialize)]
struct IdbOpenResult {
#[serde(rename = "needsUpgrade")]
needs_upgrade: bool,
#[serde(rename = "oldVersion")]
old_version: u32,
#[serde(rename = "objectStores")]
object_stores: Vec<String>,
}
/// Open an IndexedDB database.
#[op2(async)]
#[serde]
async fn op_idb_open(
state: Rc<RefCell<OpState>>,
#[string] name: String,
version: u32,
) -> Result<IdbOpenResult, deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
let result = storage.open_database(&name, version).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB open failed: {}", e))
})?;
Ok(IdbOpenResult {
needs_upgrade: result.needs_upgrade,
old_version: result.old_version,
object_stores: result.object_stores,
})
}
/// Close an IndexedDB database.
#[op2(fast)]
fn op_idb_close(state: &mut OpState, #[string] name: &str) {
let storage = state.borrow::<IdbStorage>();
storage.close_database(name);
}
/// Delete an IndexedDB database.
#[op2(async)]
async fn op_idb_delete_database(
state: Rc<RefCell<OpState>>,
#[string] name: String,
) -> Result<(), deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.delete_database(&name).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB delete database failed: {}", e))
})
}
#[derive(Deserialize)]
struct CreateObjectStoreOptions {
#[serde(rename = "keyPath")]
key_path: Option<String>,
#[serde(rename = "autoIncrement")]
auto_increment: Option<bool>,
}
/// Create an object store.
#[op2]
fn op_idb_create_object_store(
state: &mut OpState,
#[string] db_name: &str,
#[string] store_name: &str,
#[serde] options: CreateObjectStoreOptions,
) -> Result<(), deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
storage
.create_object_store(
db_name,
store_name,
options.key_path.as_deref(),
options.auto_increment.unwrap_or(false),
)
.map_err(|e| deno_core::error::generic_error(format!("Create object store failed: {}", e)))
}
/// Delete an object store.
#[op2(fast)]
fn op_idb_delete_object_store(
state: &mut OpState,
#[string] db_name: &str,
#[string] store_name: &str,
) -> Result<(), deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
storage
.delete_object_store(db_name, store_name)
.map_err(|e| deno_core::error::generic_error(format!("Delete object store failed: {}", e)))
}
/// Get a value from an object store.
#[op2(async)]
#[serde]
async fn op_idb_get(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
#[serde] key: serde_json::Value,
) -> Result<Option<serde_json::Value>, deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.get(&db_name, &store_name, &key).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB get failed: {}", e))
})
}
/// Put a value into an object store.
#[op2(async)]
async fn op_idb_put(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
#[serde] key: serde_json::Value,
#[serde] value: serde_json::Value,
) -> Result<(), deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.put(&db_name, &store_name, &key, &value).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB put failed: {}", e))
})
}
/// Delete a value from an object store.
#[op2(async)]
async fn op_idb_delete(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
#[serde] key: serde_json::Value,
) -> Result<(), deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.delete(&db_name, &store_name, &key).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB delete failed: {}", e))
})
}
/// Clear all values from an object store.
#[op2(async)]
async fn op_idb_clear(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
) -> Result<(), deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.clear(&db_name, &store_name).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB clear failed: {}", e))
})
}
/// Get all values from an object store.
#[op2(async)]
#[serde]
async fn op_idb_get_all(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
count: Option<u32>,
) -> Result<Vec<serde_json::Value>, deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.get_all(&db_name, &store_name, count).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB get_all failed: {}", e))
})
}
/// Get all keys from an object store.
#[op2(async)]
#[serde]
async fn op_idb_get_all_keys(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
count: Option<u32>,
) -> Result<Vec<serde_json::Value>, deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.get_all_keys(&db_name, &store_name, count).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB get_all_keys failed: {}", e))
})
}
/// Count values in an object store.
#[op2(async)]
async fn op_idb_count(
state: Rc<RefCell<OpState>>,
#[string] db_name: String,
#[string] store_name: String,
) -> Result<u32, deno_core::error::AnyError> {
let storage = {
let state = state.borrow();
state.borrow::<IdbStorage>().clone()
};
storage.count(&db_name, &store_name).await.map_err(|e| {
deno_core::error::generic_error(format!("IDB count failed: {}", e))
})
}
// ============================================================================
// Skill Bridge Ops (db, store, net)
// ============================================================================
#[op2]
#[bigint]
fn op_db_exec(
state: &mut OpState,
#[string] sql: &str,
#[string] params_json: Option<String>,
) -> Result<i64, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
let params: Vec<serde_json::Value> = match params_json {
Some(p) => serde_json::from_str(&p).unwrap_or_default(),
None => Vec::new(),
};
storage
.skill_db_exec(&ctx.skill_id, sql, &params)
.map(|n| n as i64)
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_db_get(
state: &mut OpState,
#[string] sql: &str,
#[string] params_json: Option<String>,
) -> Result<String, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
let params: Vec<serde_json::Value> = match params_json {
Some(p) => serde_json::from_str(&p).unwrap_or_default(),
None => Vec::new(),
};
storage
.skill_db_get(&ctx.skill_id, sql, &params)
.map(|v| v.to_string())
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_db_all(
state: &mut OpState,
#[string] sql: &str,
#[string] params_json: Option<String>,
) -> Result<String, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
let params: Vec<serde_json::Value> = match params_json {
Some(p) => serde_json::from_str(&p).unwrap_or_default(),
None => Vec::new(),
};
storage
.skill_db_all(&ctx.skill_id, sql, &params)
.map(|v| v.to_string())
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_db_kv_get(state: &mut OpState, #[string] key: &str) -> Result<String, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
storage
.skill_kv_get(&ctx.skill_id, key)
.map(|v| v.to_string())
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2(fast)]
fn op_db_kv_set(
state: &mut OpState,
#[string] key: &str,
#[string] value_json: &str,
) -> Result<(), deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
let value: serde_json::Value =
serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null);
storage
.skill_kv_set(&ctx.skill_id, key, &value)
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_store_get(state: &mut OpState, #[string] key: &str) -> Result<String, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
storage
.skill_store_get(&ctx.skill_id, key)
.map(|v| v.to_string())
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2(fast)]
fn op_store_set(
state: &mut OpState,
#[string] key: &str,
#[string] value_json: &str,
) -> Result<(), deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
let value: serde_json::Value =
serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null);
storage
.skill_store_set(&ctx.skill_id, key, &value)
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2(fast)]
fn op_store_delete(state: &mut OpState, #[string] key: &str) -> Result<(), deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
storage
.skill_store_delete(&ctx.skill_id, key)
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_store_keys(state: &mut OpState) -> Result<String, deno_core::error::AnyError> {
let storage = state.borrow::<IdbStorage>();
let ctx = state.borrow::<SkillContext>();
storage
.skill_store_keys(&ctx.skill_id)
.map(|keys| serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string()))
.map_err(|e| deno_core::error::generic_error(e))
}
#[op2]
#[string]
fn op_net_fetch(
#[string] url: &str,
#[string] options_json: &str,
) -> Result<String, deno_core::error::AnyError> {
crate::runtime::bridge::net::http_fetch(url, options_json)
.map_err(|e| deno_core::error::generic_error(e))
}
// ============================================================================
// State Bridge Ops
// ============================================================================
/// Get a value from the skill's published state.
#[op2]
#[string]
fn op_state_get(state: &mut OpState, #[string] key: &str) -> Result<String, deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
let skill_state = ctx.skill_state.as_ref().ok_or_else(|| {
deno_core::error::generic_error("Skill state not initialized")
})?;
let published_state = &skill_state.read().published_state;
let value = published_state.get(key).cloned().unwrap_or(serde_json::Value::Null);
Ok(value.to_string())
}
/// Set a value in the skill's published state.
#[op2(fast)]
fn op_state_set(
state: &mut OpState,
#[string] key: &str,
#[string] value_json: &str,
) -> Result<(), deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
let skill_state = ctx.skill_state.as_ref().ok_or_else(|| {
deno_core::error::generic_error("Skill state not initialized")
})?;
let value: serde_json::Value =
serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null);
skill_state.write().published_state.insert(key.to_string(), value);
Ok(())
}
/// Merge a partial object into the skill's published state.
#[op2(fast)]
fn op_state_set_partial(
state: &mut OpState,
#[string] partial_json: &str,
) -> Result<(), deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
let skill_state = ctx.skill_state.as_ref().ok_or_else(|| {
deno_core::error::generic_error("Skill state not initialized")
})?;
let partial: serde_json::Value =
serde_json::from_str(partial_json).unwrap_or(serde_json::Value::Object(Default::default()));
if let serde_json::Value::Object(map) = partial {
let mut state_guard = skill_state.write();
for (k, v) in map {
state_guard.published_state.insert(k, v);
}
}
Ok(())
}
// ============================================================================
// Data Bridge Ops
// ============================================================================
/// Read a file from the skill's data directory.
#[op2]
#[string]
fn op_data_read(
state: &mut OpState,
#[string] filename: &str,
) -> Result<String, deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
let data_dir = ctx.data_dir.as_ref().ok_or_else(|| {
deno_core::error::generic_error("Data directory not configured")
})?;
let path = data_dir.join(filename);
// Prevent path traversal
if !path.starts_with(data_dir) {
return Err(deno_core::error::generic_error("Invalid filename: path traversal"));
}
std::fs::read_to_string(&path).map_err(|e| {
deno_core::error::generic_error(format!("Failed to read file '{}': {}", filename, e))
})
}
/// Write a file to the skill's data directory.
#[op2(fast)]
fn op_data_write(
state: &mut OpState,
#[string] filename: &str,
#[string] content: &str,
) -> Result<(), deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
let data_dir = ctx.data_dir.as_ref().ok_or_else(|| {
deno_core::error::generic_error("Data directory not configured")
})?;
let path = data_dir.join(filename);
// Prevent path traversal
if !path.starts_with(data_dir) {
return Err(deno_core::error::generic_error("Invalid filename: path traversal"));
}
// Ensure parent directories exist
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
deno_core::error::generic_error(format!("Failed to create directory: {}", e))
})?;
}
std::fs::write(&path, content).map_err(|e| {
deno_core::error::generic_error(format!("Failed to write file '{}': {}", filename, e))
})
}
// ============================================================================
// TDLib Ops (telegram skill only)
// ============================================================================
/// Check if the current skill is the telegram skill.
fn check_telegram_skill(state: &OpState) -> Result<(), deno_core::error::AnyError> {
let ctx = state.borrow::<SkillContext>();
if ctx.skill_id != "telegram" {
return Err(deno_core::error::generic_error(
"TDLib is only available to the telegram skill",
));
}
Ok(())
}
/// Check if TDLib is available (always true on desktop).
#[op2(fast)]
fn op_tdlib_is_available() -> bool {
true
}
/// Create a TDLib client with the given data directory.
/// This is synchronous since create_client just spawns a worker thread and returns.
#[op2(fast)]
fn op_tdlib_create_client(
state: &mut OpState,
#[string] data_dir: String,
) -> Result<i32, deno_core::error::AnyError> {
// Check skill permission
check_telegram_skill(state)?;
let path = std::path::PathBuf::from(data_dir);
crate::services::tdlib::TDLIB_MANAGER
.create_client(path)
.map_err(|e| deno_core::error::generic_error(e))
}
/// Send a request to TDLib and wait for the response.
#[op2(async)]
#[serde]
async fn op_tdlib_send(
state: Rc<RefCell<OpState>>,
#[serde] request: serde_json::Value,
) -> Result<serde_json::Value, deno_core::error::AnyError> {
// Check skill permission
{
let state = state.borrow();
check_telegram_skill(&state)?;
}
crate::services::tdlib::TDLIB_MANAGER
.send(request)
.await
.map_err(|e| deno_core::error::generic_error(e))
}
/// Receive the next update from TDLib (with timeout in ms).
#[op2(async)]
#[serde]
async fn op_tdlib_receive(
state: Rc<RefCell<OpState>>,
timeout_ms: u32,
) -> Result<Option<serde_json::Value>, deno_core::error::AnyError> {
// Check skill permission
{
let state = state.borrow();
check_telegram_skill(&state)?;
}
Ok(crate::services::tdlib::TDLIB_MANAGER.receive(timeout_ms).await)
}
/// Destroy the TDLib client and clean up resources.
#[op2(async)]
async fn op_tdlib_destroy(
state: Rc<RefCell<OpState>>,
) -> Result<(), deno_core::error::AnyError> {
// Check skill permission
{
let state = state.borrow();
check_telegram_skill(&state)?;
}
crate::services::tdlib::TDLIB_MANAGER
.destroy()
.await
.map_err(|e| deno_core::error::generic_error(e))
}
// ============================================================================
// Model Ops (local LLM inference)
// ============================================================================
/// Check if local model API is available (desktop only).
#[op2(fast)]
fn op_model_is_available() -> bool {
true
}
/// Get model status (loading, ready, error).
#[op2]
#[serde]
fn op_model_get_status() -> serde_json::Value {
let status = crate::services::llama::LLAMA_MANAGER.get_status();
serde_json::to_value(status).unwrap_or_default()
}
/// Generate text from prompt (async, blocking inference on thread pool).
#[op2(async)]
#[string]
async fn op_model_generate(
#[string] prompt: String,
#[serde] config: serde_json::Value,
) -> Result<String, deno_core::error::AnyError> {
let cfg: crate::services::llama::GenerateConfig =
serde_json::from_value(config).unwrap_or_default();
crate::services::llama::LLAMA_MANAGER
.generate(&prompt, cfg)
.await
.map_err(|e| deno_core::error::generic_error(e))
}
/// Summarize text (async).
#[op2(async)]
#[string]
async fn op_model_summarize(
#[string] text: String,
max_tokens: u32,
) -> Result<String, deno_core::error::AnyError> {
crate::services::llama::LLAMA_MANAGER
.summarize(&text, max_tokens)
.await
.map_err(|e| deno_core::error::generic_error(e))
}