mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
* 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>
1237 lines
36 KiB
Rust
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, ¶ms)
|
|
.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, ¶ms)
|
|
.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, ¶ms)
|
|
.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))
|
|
}
|