Fix/prod build (#51)

* chore: add CI secrets management and local testing script

- Added ci-secrets.example.json to provide a template for CI secrets configuration.
- Introduced test-ci-local.sh script to facilitate local testing of the package-and-publish workflow using act.
- Updated .gitignore to exclude the actual ci-secrets.json file containing sensitive tokens.

* chore: enhance local testing and environment loading scripts

- Added scripts to load environment variables from .env and JSON files, improving local development setup.
- Introduced ci-event.json to simulate GitHub event payloads for local CI testing.
- Updated test-ci-local.sh to utilize the new event JSON for better integration with local testing workflows.
- Modified .gitignore to include ci-secrets.local.json for local secret management.

* chore: enhance environment loading and improve V8 memory management

- Updated load-env.sh to conditionally load ci-secrets.local.json and set APPLE_PASSWORD for notarization.
- Introduced a delay in auto-starting skills to prevent memory spikes during initialization.
- Adjusted V8 runtime memory settings to minimize initial heap allocation, reducing the risk of OOM errors.

* chore: update version bump workflow to modify tauri.conf.json

- Changed the version update process to reflect changes in tauri.conf.json instead of Cargo.toml.
- Adjusted the commit message to indicate the inclusion of tauri.conf.json in the version bump process.

* chore: migrate from V8 to QuickJS runtime and update related configurations

- Replaced V8 runtime references with QuickJS throughout the codebase, including updates to initialization and error handling.
- Modified package.json to change the build command for macOS to source environment variables correctly.
- Updated Cargo.toml to remove deno_core dependency and include rquickjs with appropriate features.
- Cleaned up unused V8-related code and comments in the runtime modules.
- Adjusted skill manifest checks to support QuickJS compatibility.

* refactor: implement QuickJS runtime and remove V8 references

- Replaced V8 engine with QuickJS in the runtime module, updating related imports and initialization logic.
- Introduced new `qjs_engine.rs` and `qjs_skill_instance.rs` files to handle QuickJS-specific functionality.
- Removed the deprecated `v8_skill_instance.rs` and associated V8-related code.
- Updated JavaScript bootstrap code to align with QuickJS operations and APIs.
- Adjusted documentation and comments to reflect the transition to QuickJS.

* refactor: update IdbStorage to use parking_lot::Mutex for improved concurrency

- Changed the connection management in IdbStorage from RwLock to parking_lot::Mutex for better performance.
- Updated related methods to reflect the new locking mechanism, enhancing the efficiency of database operations.
- Adjusted the IdbOpenResult struct to include serde::Serialize for potential serialization needs.

* refactor: update QuickJS skill instance and timer management

- Modified memory limit and stack size settings in QjsSkillInstance to be asynchronous, improving performance.
- Cleaned up imports in qjs_ops.rs by removing unused dependencies and enhancing function definitions for clarity.
- Refactored timer management functions for better readability and efficiency, including renaming and restructuring comments.

* chore: update package.json scripts and clean up build.rs

- Added a new script for running the Tauri app with environment variable loading.
- Simplified the macOS development command to use a dedicated build script.
- Removed unnecessary environment variable logging from build.rs to streamline the build process.
- Cleaned up commented-out sections in the GitHub Actions workflow for Android packaging.
This commit is contained in:
Steven Enamakel
2026-02-05 19:29:21 +05:30
committed by GitHub
parent 29d62544d6
commit ceb03fb2bc
32 changed files with 2010 additions and 2985 deletions
+67 -67
View File
@@ -1,5 +1,5 @@
/**
* Bootstrap JavaScript for V8 Runtime
* Bootstrap JavaScript for QuickJS Runtime
*
* Provides browser-like API shims that tdweb expects.
* These shims call Rust "ops" for actual I/O.
@@ -14,19 +14,19 @@ globalThis.window = globalThis;
// ============================================================================
globalThis.console = {
log: function (...args) {
Deno.core.ops.op_console_log(args.map(String).join(' '));
__ops.console_log(args.map(String).join(' '));
},
info: function (...args) {
Deno.core.ops.op_console_log(args.map(String).join(' '));
__ops.console_log(args.map(String).join(' '));
},
warn: function (...args) {
Deno.core.ops.op_console_warn(args.map(String).join(' '));
__ops.console_warn(args.map(String).join(' '));
},
error: function (...args) {
Deno.core.ops.op_console_error(args.map(String).join(' '));
__ops.console_error(args.map(String).join(' '));
},
debug: function (...args) {
Deno.core.ops.op_console_log('[DEBUG] ' + args.map(String).join(' '));
__ops.console_log('[DEBUG] ' + args.map(String).join(' '));
},
};
@@ -39,25 +39,25 @@ let nextTimerId = 1;
globalThis.setTimeout = function (callback, delay, ...args) {
const id = nextTimerId++;
timerCallbacks.set(id, { callback, args, type: 'timeout' });
Deno.core.ops.op_ah_timer_start(id, delay || 0, false);
__ops.timer_start(id, delay || 0, false);
return id;
};
globalThis.setInterval = function (callback, delay, ...args) {
const id = nextTimerId++;
timerCallbacks.set(id, { callback, args, type: 'interval' });
Deno.core.ops.op_ah_timer_start(id, delay || 0, true);
__ops.timer_start(id, delay || 0, true);
return id;
};
globalThis.clearTimeout = function (id) {
timerCallbacks.delete(id);
Deno.core.ops.op_ah_timer_cancel(id);
__ops.timer_cancel(id);
};
globalThis.clearInterval = function (id) {
timerCallbacks.delete(id);
Deno.core.ops.op_ah_timer_cancel(id);
__ops.timer_cancel(id);
};
// Timer callback handler (called from Rust)
@@ -146,7 +146,7 @@ globalThis.fetch = async function (url, options = {}) {
headersObj = headers;
}
const result = await Deno.core.ops.op_fetch(url.toString(), {
const result = await __ops.fetch(url.toString(), {
method,
headers: headersObj,
body: typeof body === 'string' ? body : body ? JSON.stringify(body) : null,
@@ -259,7 +259,7 @@ class WebSocket {
async _connect() {
try {
this._id = await Deno.core.ops.op_ws_connect(this.url);
this._id = await __ops.ws_connect(this.url);
this.readyState = WebSocket.OPEN;
// Register for message callbacks
@@ -282,7 +282,7 @@ class WebSocket {
async _startReceiving() {
while (this.readyState === WebSocket.OPEN) {
try {
const message = await Deno.core.ops.op_ws_recv(this._id);
const message = await __ops.ws_recv(this._id);
if (message === null) {
// Connection closed
this._handleClose(1000, '');
@@ -322,7 +322,7 @@ class WebSocket {
}
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
Deno.core.ops.op_ws_send(this._id, dataStr);
__ops.ws_send(this._id, dataStr);
}
close(code = 1000, reason = '') {
@@ -331,7 +331,7 @@ class WebSocket {
}
this.readyState = WebSocket.CLOSING;
Deno.core.ops.op_ws_close(this._id, code, reason);
__ops.ws_close(this._id, code, reason);
}
}
@@ -350,7 +350,7 @@ class IDBFactory {
const request = new IDBRequest();
(async () => {
try {
await Deno.core.ops.op_idb_delete_database(name);
await __ops.idb_delete_database(name);
request._success(undefined);
} catch (e) {
request._error(e);
@@ -398,7 +398,7 @@ class IDBOpenDBRequest extends IDBRequest {
async _open() {
try {
const info = await Deno.core.ops.op_idb_open(this._name, this._version);
const info = await __ops.idb_open(this._name, this._version);
const db = new IDBDatabase(this._name, this._version, info.objectStores || []);
if (info.needsUpgrade) {
@@ -433,13 +433,13 @@ class IDBDatabase {
}
createObjectStore(name, options = {}) {
Deno.core.ops.op_idb_create_object_store(this.name, name, options);
__ops.idb_create_object_store(this.name, name, options);
this.objectStoreNames.push(name);
return new IDBObjectStore(this, name);
}
deleteObjectStore(name) {
Deno.core.ops.op_idb_delete_object_store(this.name, name);
__ops.idb_delete_object_store(this.name, name);
const idx = this.objectStoreNames.indexOf(name);
if (idx >= 0) this.objectStoreNames.splice(idx, 1);
}
@@ -449,7 +449,7 @@ class IDBDatabase {
}
close() {
Deno.core.ops.op_idb_close(this.name);
__ops.idb_close(this.name);
}
}
@@ -495,7 +495,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
const value = await Deno.core.ops.op_idb_get(this._db.name, this.name, key);
const value = await __ops.idb_get(this._db.name, this.name, key);
request._success(value);
} catch (e) {
request._error(e);
@@ -508,7 +508,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
await Deno.core.ops.op_idb_put(this._db.name, this.name, key, value);
await __ops.idb_put(this._db.name, this.name, key, value);
request._success(key);
} catch (e) {
request._error(e);
@@ -525,7 +525,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
await Deno.core.ops.op_idb_delete(this._db.name, this.name, key);
await __ops.idb_delete(this._db.name, this.name, key);
request._success(undefined);
} catch (e) {
request._error(e);
@@ -538,7 +538,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
await Deno.core.ops.op_idb_clear(this._db.name, this.name);
await __ops.idb_clear(this._db.name, this.name);
request._success(undefined);
} catch (e) {
request._error(e);
@@ -551,7 +551,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
const values = await Deno.core.ops.op_idb_get_all(this._db.name, this.name, count);
const values = await __ops.idb_get_all(this._db.name, this.name, count);
request._success(values);
} catch (e) {
request._error(e);
@@ -564,7 +564,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
const keys = await Deno.core.ops.op_idb_get_all_keys(this._db.name, this.name, count);
const keys = await __ops.idb_get_all_keys(this._db.name, this.name, count);
request._success(keys);
} catch (e) {
request._error(e);
@@ -577,7 +577,7 @@ class IDBObjectStore {
const request = new IDBRequest();
(async () => {
try {
const count = await Deno.core.ops.op_idb_count(this._db.name, this.name);
const count = await __ops.idb_count(this._db.name, this.name);
request._success(count);
} catch (e) {
request._error(e);
@@ -636,13 +636,13 @@ if (typeof globalThis.TextDecoder === 'undefined') {
// ============================================================================
if (typeof globalThis.atob === 'undefined') {
globalThis.atob = function (str) {
return Deno.core.ops.op_atob(str);
return __ops.atob(str);
};
}
if (typeof globalThis.btoa === 'undefined') {
globalThis.btoa = function (str) {
return Deno.core.ops.op_btoa(str);
return __ops.btoa(str);
};
}
@@ -651,7 +651,7 @@ if (typeof globalThis.btoa === 'undefined') {
// ============================================================================
globalThis.crypto = {
getRandomValues: function (array) {
const bytes = Deno.core.ops.op_crypto_random(array.length);
const bytes = __ops.crypto_random(array.length);
array.set(bytes);
return array;
},
@@ -662,7 +662,7 @@ globalThis.crypto = {
// ============================================================================
globalThis.performance = {
now: function () {
return Deno.core.ops.op_performance_now();
return __ops.performance_now();
},
};
@@ -673,53 +673,53 @@ globalThis.performance = {
globalThis.__db = {
exec: function (sql, paramsJson) {
return Deno.core.ops.op_db_exec(sql, paramsJson);
return __ops.db_exec(sql, paramsJson);
},
get: function (sql, paramsJson) {
return Deno.core.ops.op_db_get(sql, paramsJson);
return __ops.db_get(sql, paramsJson);
},
all: function (sql, paramsJson) {
return Deno.core.ops.op_db_all(sql, paramsJson);
return __ops.db_all(sql, paramsJson);
},
kvGet: function (key) {
return Deno.core.ops.op_db_kv_get(key);
return __ops.db_kv_get(key);
},
kvSet: function (key, valueJson) {
return Deno.core.ops.op_db_kv_set(key, valueJson);
return __ops.db_kv_set(key, valueJson);
},
};
globalThis.__store = {
get: function (key) {
return Deno.core.ops.op_store_get(key);
return __ops.store_get(key);
},
set: function (key, valueJson) {
return Deno.core.ops.op_store_set(key, valueJson);
return __ops.store_set(key, valueJson);
},
delete: function (key) {
return Deno.core.ops.op_store_delete(key);
return __ops.store_delete(key);
},
keys: function () {
return Deno.core.ops.op_store_keys();
return __ops.store_keys();
},
};
globalThis.__net = {
fetch: function (url, optionsJson) {
return Deno.core.ops.op_net_fetch(url, optionsJson);
return __ops.net_fetch(url, optionsJson);
},
};
globalThis.__platform = {
os: function () {
return Deno.core.ops.op_platform_os();
return __ops.platform_os();
},
env: function (key) {
return Deno.core.ops.op_platform_env(key);
return __ops.platform_env(key);
},
};
// High-level wrappers for skills (V8 bridge)
// High-level wrappers for skills (QuickJS bridge)
globalThis.db = {
exec: function (sql, params) {
return __db.exec(sql, params ? JSON.stringify(params) : undefined);
@@ -779,13 +779,13 @@ globalThis.platform = {
// ============================================================================
globalThis.__state = {
get: function (key) {
return Deno.core.ops.op_state_get(key);
return __ops.state_get(key);
},
set: function (key, valueJson) {
return Deno.core.ops.op_state_set(key, valueJson);
return __ops.state_set(key, valueJson);
},
setPartial: function (partialJson) {
return Deno.core.ops.op_state_set_partial(partialJson);
return __ops.state_set_partial(partialJson);
},
};
@@ -807,10 +807,10 @@ globalThis.state = {
// ============================================================================
globalThis.__data = {
read: function (filename) {
return Deno.core.ops.op_data_read(filename);
return __ops.data_read(filename);
},
write: function (filename, content) {
return Deno.core.ops.op_data_write(filename, content);
return __ops.data_write(filename, content);
},
};
@@ -828,15 +828,15 @@ globalThis.data = {
// ============================================================================
globalThis.cron = {
register: function (scheduleId, cronExpr) {
console.warn('[cron] register not implemented in V8 runtime yet');
console.warn('[cron] register not implemented in QuickJS runtime yet');
return false;
},
unregister: function (scheduleId) {
console.warn('[cron] unregister not implemented in V8 runtime yet');
console.warn('[cron] unregister not implemented in QuickJS runtime yet');
return false;
},
list: function () {
console.warn('[cron] list not implemented in V8 runtime yet');
console.warn('[cron] list not implemented in QuickJS runtime yet');
return [];
},
};
@@ -846,11 +846,11 @@ globalThis.cron = {
// ============================================================================
globalThis.skills = {
list: function () {
console.warn('[skills] list not implemented in V8 runtime yet');
console.warn('[skills] list not implemented in QuickJS runtime yet');
return [];
},
callTool: function (skillId, toolName, args) {
console.warn('[skills] callTool not implemented in V8 runtime yet');
console.warn('[skills] callTool not implemented in QuickJS runtime yet');
return { error: 'Not implemented' };
},
};
@@ -868,8 +868,8 @@ globalThis.tdlib = {
*/
isAvailable: function () {
try {
return typeof Deno?.core?.ops?.op_tdlib_is_available === 'function'
? Deno.core.ops.op_tdlib_is_available()
return typeof __ops?.tdlib_is_available === 'function'
? __ops.tdlib_is_available()
: false;
} catch (e) {
return false;
@@ -881,8 +881,8 @@ globalThis.tdlib = {
* @param {string} dataDir - Path to store TDLib data files.
* @returns {Promise<number>} Client ID (always 1 for singleton).
*/
createClient: async function (dataDir) {
return await Deno.core.ops.op_tdlib_create_client(dataDir);
createClient: function (dataDir) {
return __ops.tdlib_create_client(dataDir);
},
/**
@@ -891,7 +891,7 @@ globalThis.tdlib = {
* @returns {Promise<object>} TDLib response object.
*/
send: async function (request) {
return await Deno.core.ops.op_tdlib_send(request);
return await __ops.tdlib_send(request);
},
/**
@@ -900,7 +900,7 @@ globalThis.tdlib = {
* @returns {Promise<object|null>} Update object or null if timeout.
*/
receive: async function (timeoutMs = 1000) {
return await Deno.core.ops.op_tdlib_receive(timeoutMs);
return await __ops.tdlib_receive(timeoutMs);
},
/**
@@ -908,7 +908,7 @@ globalThis.tdlib = {
* @returns {Promise<void>}
*/
destroy: async function () {
return await Deno.core.ops.op_tdlib_destroy();
return await __ops.tdlib_destroy();
},
};
@@ -919,21 +919,21 @@ globalThis.tdlib = {
globalThis.__model = {
isAvailable: function () {
try {
return typeof Deno?.core?.ops?.op_model_is_available === 'function'
? Deno.core.ops.op_model_is_available()
return typeof __ops?.model_is_available === 'function'
? __ops.model_is_available()
: false;
} catch (e) {
return false;
}
},
getStatus: function () {
return Deno.core.ops.op_model_get_status();
return __ops.model_get_status();
},
generate: async function (prompt, configJson) {
return await Deno.core.ops.op_model_generate(prompt, configJson);
return await __ops.model_generate(prompt, configJson);
},
summarize: async function (text, maxTokens) {
return await Deno.core.ops.op_model_summarize(text, maxTokens);
return await __ops.model_summarize(text, maxTokens);
},
};
@@ -986,4 +986,4 @@ globalThis.model = {
};
console.log('[bootstrap] Model API initialized');
console.log('[bootstrap] V8 browser APIs initialized');
console.log('[bootstrap] QuickJS browser APIs initialized');
+5 -5
View File
@@ -1,10 +1,10 @@
//! TDLib V8 Runtime Module
//! TDLib Runtime Module
//!
//! Provides a V8 JavaScript runtime (via deno_core) for running
//! skill JavaScript code and TDLib via tdweb. Provides a browser-like
//! environment that supports WASM natively.
//! Provides a QuickJS JavaScript runtime (via rquickjs) for running
//! skill JavaScript code and TDLib integration. Provides a browser-like
//! environment for skill execution.
pub mod ops;
pub mod qjs_ops;
pub mod service;
pub mod storage;
File diff suppressed because it is too large Load Diff
+799
View File
@@ -0,0 +1,799 @@
use parking_lot::RwLock;
use rquickjs::{function::Async, Ctx, Function, Object, Result as JsResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::storage::IdbStorage;
// ============================================================================
// Timer State
// ============================================================================
#[derive(Debug)]
pub struct TimerEntry {
pub deadline: Instant,
pub delay_ms: u32,
pub is_interval: bool,
}
#[derive(Debug, Default)]
pub struct TimerState {
pub timers: HashMap<u32, TimerEntry>,
}
impl TimerState {
pub fn poll_ready(&mut self) -> Vec<u32> {
let now = Instant::now();
let mut ready = Vec::new();
let mut to_remove = Vec::new();
for (&id, entry) in &self.timers {
if now >= entry.deadline {
ready.push(id);
if !entry.is_interval {
to_remove.push(id);
}
}
}
for id in to_remove {
self.timers.remove(&id);
}
for &id in &ready {
if let Some(entry) = self.timers.get_mut(&id) {
if entry.is_interval {
entry.deadline = now + Duration::from_millis(entry.delay_ms as u64);
}
}
}
ready
}
pub fn time_until_next(&self) -> Option<Duration> {
let now = Instant::now();
self.timers
.values()
.map(|e| e.deadline.saturating_duration_since(now))
.min()
}
}
pub fn poll_timers(timer_state: &RwLock<TimerState>) -> (Vec<u32>, Option<Duration>) {
let mut ts = timer_state.write();
let ready = ts.poll_ready();
let next = ts.time_until_next();
(ready, next)
}
// ============================================================================
// Skill Context
// ============================================================================
#[derive(Clone)]
pub struct SkillContext {
pub skill_id: String,
pub data_dir: PathBuf,
}
// ============================================================================
// Skill State (shared published state)
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillState {
#[serde(flatten)]
pub data: serde_json::Map<String, serde_json::Value>,
}
impl Default for SkillState {
fn default() -> Self {
Self {
data: serde_json::Map::new(),
}
}
}
// ============================================================================
// WebSocket State (placeholder)
// ============================================================================
#[derive(Debug)]
pub struct WebSocketConnection {
pub url: String,
}
#[derive(Debug, Default)]
pub struct WebSocketState {
pub connections: HashMap<u32, WebSocketConnection>,
pub next_id: u32,
}
// ============================================================================
// Allowed Environment Variables
// ============================================================================
const ALLOWED_ENV_VARS: &[&str] = &[
"VITE_BACKEND_URL",
"VITE_TELEGRAM_API_ID",
"VITE_TELEGRAM_API_HASH",
"VITE_TELEGRAM_BOT_USERNAME",
"VITE_TELEGRAM_BOT_ID",
"NODE_ENV",
];
// ============================================================================
// Helpers
// ============================================================================
fn check_telegram_skill(skill_id: &str) -> Result<(), String> {
if skill_id != "telegram" {
Err("TDLib operations only available in telegram skill".to_string())
} else {
Ok(())
}
}
fn js_err(msg: String) -> rquickjs::Error {
rquickjs::Error::new_from_js_message("ops", "Error", msg)
}
// ============================================================================
// Main Registration Function
// ============================================================================
pub fn register_ops(
ctx: &Ctx<'_>,
storage: IdbStorage,
skill_context: SkillContext,
skill_state: Arc<RwLock<SkillState>>,
timer_state: Arc<RwLock<TimerState>>,
ws_state: Arc<RwLock<WebSocketState>>,
) -> JsResult<()> {
let globals = ctx.globals();
let ops = Object::new(ctx.clone())?;
// ========================================================================
// Console (3)
// ========================================================================
ops.set("console_log", Function::new(ctx.clone(), |msg: String| {
log::info!("[js] {}", msg);
}))?;
ops.set("console_warn", Function::new(ctx.clone(), |msg: String| {
log::warn!("[js] {}", msg);
}))?;
ops.set("console_error", Function::new(ctx.clone(), |msg: String| {
log::error!("[js] {}", msg);
}))?;
// ========================================================================
// Crypto (3)
// ========================================================================
ops.set("crypto_random", Function::new(ctx.clone(), |len: usize| -> Vec<u8> {
use rand::RngCore;
let mut buf = vec![0u8; len];
rand::thread_rng().fill_bytes(&mut buf);
buf
}))?;
ops.set("atob", Function::new(ctx.clone(), |input: String| -> rquickjs::Result<String> {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&input)
.map_err(|e| js_err(e.to_string()))?;
String::from_utf8(bytes).map_err(|e| js_err(e.to_string()))
}))?;
ops.set("btoa", Function::new(ctx.clone(), |input: String| -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(input.as_bytes())
}))?;
// ========================================================================
// Performance (1)
// ========================================================================
ops.set("performance_now", Function::new(ctx.clone(), || -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
* 1000.0
}))?;
// ========================================================================
// Platform (2)
// ========================================================================
ops.set("platform_os", Function::new(ctx.clone(), || -> &'static str {
if cfg!(target_os = "windows") { "windows" }
else if cfg!(target_os = "macos") { "macos" }
else if cfg!(target_os = "linux") { "linux" }
else if cfg!(target_os = "android") { "android" }
else if cfg!(target_os = "ios") { "ios" }
else { "unknown" }
}))?;
ops.set("platform_env", Function::new(ctx.clone(), |key: String| -> Option<String> {
if ALLOWED_ENV_VARS.contains(&key.as_str()) {
std::env::var(&key).ok()
} else {
None
}
}))?;
// ========================================================================
// Timers (2)
// ========================================================================
{
let ts = timer_state.clone();
ops.set("timer_start", Function::new(ctx.clone(),
move |id: u32, delay_ms: u32, is_interval: bool| {
let mut state = ts.write();
state.timers.insert(id, TimerEntry {
deadline: Instant::now() + Duration::from_millis(delay_ms as u64),
delay_ms,
is_interval,
});
},
))?;
}
{
let ts = timer_state.clone();
ops.set("timer_cancel", Function::new(ctx.clone(), move |id: u32| {
let mut state = ts.write();
state.timers.remove(&id);
}))?;
}
// ========================================================================
// Fetch (1) - ASYNC
// ========================================================================
ops.set("fetch", Function::new(ctx.clone(),
Async(move |url: String, options: String| async move {
let opts: serde_json::Value =
serde_json::from_str(&options).map_err(|e| js_err(e.to_string()))?;
let method = opts["method"].as_str().unwrap_or("GET");
let headers_obj = opts["headers"].as_object();
let body = opts["body"].as_str();
let client = reqwest::Client::new();
let mut req = match method {
"GET" => client.get(&url),
"POST" => client.post(&url),
"PUT" => client.put(&url),
"DELETE" => client.delete(&url),
_ => client.get(&url),
};
if let Some(h) = headers_obj {
for (k, v) in h {
if let Some(val_str) = v.as_str() {
req = req.header(k, val_str);
}
}
}
if let Some(b) = body {
req = req.body(b.to_string());
}
let response = req.send().await.map_err(|e| js_err(e.to_string()))?;
let status = response.status().as_u16();
let status_text = response.status().canonical_reason().unwrap_or("").to_string();
let headers: HashMap<String, String> = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body_text = response.text().await.map_err(|e| js_err(e.to_string()))?;
let result = serde_json::json!({
"status": status,
"statusText": status_text,
"headers": headers,
"body": body_text,
});
Ok::<String, rquickjs::Error>(result.to_string())
}),
))?;
// ========================================================================
// WebSocket (4) - placeholders
// ========================================================================
{
let ws = ws_state.clone();
ops.set("ws_connect", Function::new(ctx.clone(),
Async(move |url: String| {
let ws = ws.clone();
async move {
let mut state = ws.write();
let id = state.next_id;
state.next_id += 1;
state.connections.insert(id, WebSocketConnection { url });
Ok::<u32, rquickjs::Error>(id)
}
}),
))?;
}
{
let ws = ws_state.clone();
ops.set("ws_send", Function::new(ctx.clone(), move |_id: u32, _data: String| {
let _state = ws.read();
}))?;
}
{
let ws = ws_state.clone();
ops.set("ws_recv", Function::new(ctx.clone(),
Async(move |_id: u32| {
let _ws = ws.clone();
async move { Ok::<Option<String>, rquickjs::Error>(None) }
}),
))?;
}
{
let ws = ws_state.clone();
ops.set("ws_close", Function::new(ctx.clone(), move |id: u32, _code: u16, _reason: String| {
let mut state = ws.write();
state.connections.remove(&id);
}))?;
}
// ========================================================================
// IndexedDB (11) - all sync
// ========================================================================
{
let s = storage.clone();
ops.set("idb_open", Function::new(ctx.clone(),
move |name: String, version: u32| -> rquickjs::Result<String> {
let result = s.open_database(&name, version).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_close", Function::new(ctx.clone(), move |name: String| {
s.close_database(&name);
}))?;
}
{
let s = storage.clone();
ops.set("idb_delete_database", Function::new(ctx.clone(),
move |name: String| -> rquickjs::Result<()> {
s.delete_database(&name).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_create_object_store", Function::new(ctx.clone(),
move |db_name: String, store_name: String, options: String| -> rquickjs::Result<()> {
let opts: serde_json::Value =
serde_json::from_str(&options).map_err(|e| js_err(e.to_string()))?;
let key_path = opts["keyPath"].as_str();
let auto_increment = opts["autoIncrement"].as_bool().unwrap_or(false);
s.create_object_store(&db_name, &store_name, key_path, auto_increment)
.map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_delete_object_store", Function::new(ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<()> {
s.delete_object_store(&db_name, &store_name).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_get", Function::new(ctx.clone(),
move |db_name: String, store_name: String, key: String| -> rquickjs::Result<String> {
let key_val: serde_json::Value =
serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?;
let result = s.get(&db_name, &store_name, &key_val).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_put", Function::new(ctx.clone(),
move |db_name: String, store_name: String, key: String, value: String| -> rquickjs::Result<()> {
let key_val: serde_json::Value =
serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?;
let value_val: serde_json::Value =
serde_json::from_str(&value).map_err(|e| js_err(e.to_string()))?;
s.put(&db_name, &store_name, &key_val, &value_val).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_delete", Function::new(ctx.clone(),
move |db_name: String, store_name: String, key: String| -> rquickjs::Result<()> {
let key_val: serde_json::Value =
serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?;
s.delete(&db_name, &store_name, &key_val).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_clear", Function::new(ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<()> {
s.clear(&db_name, &store_name).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_get_all", Function::new(ctx.clone(),
move |db_name: String, store_name: String, count: Option<u32>| -> rquickjs::Result<String> {
let result = s.get_all(&db_name, &store_name, count).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_get_all_keys", Function::new(ctx.clone(),
move |db_name: String, store_name: String, count: Option<u32>| -> rquickjs::Result<String> {
let result = s.get_all_keys(&db_name, &store_name, count).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
ops.set("idb_count", Function::new(ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<u32> {
s.count(&db_name, &store_name).map_err(|e| js_err(e))
},
))?;
}
// ========================================================================
// DB Bridge (5)
// ========================================================================
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("db_exec", Function::new(ctx.clone(),
move |sql: String, params_json: Option<String>| -> rquickjs::Result<i64> {
let params: Vec<serde_json::Value> = if let Some(p) = params_json {
serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))?
} else {
Vec::new()
};
let rows = s.skill_db_exec(&sc.skill_id, &sql, &params).map_err(|e| js_err(e))?;
Ok(rows as i64)
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("db_get", Function::new(ctx.clone(),
move |sql: String, params_json: Option<String>| -> rquickjs::Result<String> {
let params: Vec<serde_json::Value> = if let Some(p) = params_json {
serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))?
} else {
Vec::new()
};
let result = s.skill_db_get(&sc.skill_id, &sql, &params).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("db_all", Function::new(ctx.clone(),
move |sql: String, params_json: Option<String>| -> rquickjs::Result<String> {
let params: Vec<serde_json::Value> = if let Some(p) = params_json {
serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))?
} else {
Vec::new()
};
let result = s.skill_db_all(&sc.skill_id, &sql, &params).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("db_kv_get", Function::new(ctx.clone(),
move |key: String| -> rquickjs::Result<String> {
let result = s.skill_kv_get(&sc.skill_id, &key).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("db_kv_set", Function::new(ctx.clone(),
move |key: String, value_json: String| -> rquickjs::Result<()> {
let value: serde_json::Value =
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
s.skill_kv_set(&sc.skill_id, &key, &value).map_err(|e| js_err(e))
},
))?;
}
// ========================================================================
// Store Bridge (4)
// ========================================================================
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("store_get", Function::new(ctx.clone(),
move |key: String| -> rquickjs::Result<String> {
let result = s.skill_store_get(&sc.skill_id, &key).map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("store_set", Function::new(ctx.clone(),
move |key: String, value_json: String| -> rquickjs::Result<()> {
let value: serde_json::Value =
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
s.skill_store_set(&sc.skill_id, &key, &value).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("store_delete", Function::new(ctx.clone(),
move |key: String| -> rquickjs::Result<()> {
s.skill_store_delete(&sc.skill_id, &key).map_err(|e| js_err(e))
},
))?;
}
{
let s = storage.clone();
let sc = skill_context.clone();
ops.set("store_keys", Function::new(ctx.clone(),
move || -> rquickjs::Result<String> {
let keys = s.skill_store_keys(&sc.skill_id).map_err(|e| js_err(e))?;
serde_json::to_string(&keys).map_err(|e| js_err(e.to_string()))
},
))?;
}
// ========================================================================
// Net (1)
// ========================================================================
ops.set("net_fetch", Function::new(ctx.clone(),
|url: String, options_json: String| -> rquickjs::Result<String> {
crate::runtime::bridge::net::http_fetch(&url, &options_json).map_err(|e| js_err(e))
},
))?;
// ========================================================================
// State Bridge (3)
// ========================================================================
{
let ss = skill_state.clone();
ops.set("state_get", Function::new(ctx.clone(),
move |key: String| -> rquickjs::Result<String> {
let state = ss.read();
let value = state.data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
serde_json::to_string(&value).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let ss = skill_state.clone();
ops.set("state_set", Function::new(ctx.clone(),
move |key: String, value_json: String| -> rquickjs::Result<()> {
let value: serde_json::Value =
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
let mut state = ss.write();
state.data.insert(key, value);
Ok(())
},
))?;
}
{
let ss = skill_state.clone();
ops.set("state_set_partial", Function::new(ctx.clone(),
move |partial_json: String| -> rquickjs::Result<()> {
let partial: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&partial_json).map_err(|e| js_err(e.to_string()))?;
let mut state = ss.write();
for (k, v) in partial {
state.data.insert(k, v);
}
Ok(())
},
))?;
}
// ========================================================================
// Data Bridge (2)
// ========================================================================
{
let sc = skill_context.clone();
ops.set("data_read", Function::new(ctx.clone(),
move |filename: String| -> rquickjs::Result<String> {
let path = sc.data_dir.join(&filename);
std::fs::read_to_string(&path).map_err(|e| js_err(e.to_string()))
},
))?;
}
{
let sc = skill_context.clone();
ops.set("data_write", Function::new(ctx.clone(),
move |filename: String, content: String| -> rquickjs::Result<()> {
let path = sc.data_dir.join(&filename);
std::fs::write(&path, content).map_err(|e| js_err(e.to_string()))
},
))?;
}
// ========================================================================
// TDLib (5) - gated on skill_id == "telegram"
// ========================================================================
{
let sc = skill_context.clone();
ops.set("tdlib_is_available", Function::new(ctx.clone(),
move || -> bool { sc.skill_id == "telegram" },
))?;
}
{
let sc = skill_context.clone();
ops.set("tdlib_create_client", Function::new(ctx.clone(),
move |data_dir: String| -> rquickjs::Result<i32> {
check_telegram_skill(&sc.skill_id).map_err(|e| js_err(e))?;
crate::services::tdlib::TDLIB_MANAGER
.create_client(PathBuf::from(data_dir))
.map_err(|e| js_err(e))
},
))?;
}
{
let sc = skill_context.clone();
ops.set("tdlib_send", Function::new(ctx.clone(),
Async(move |request_json: String| {
let skill_id = sc.skill_id.clone();
async move {
check_telegram_skill(&skill_id).map_err(|e| js_err(e))?;
let request: serde_json::Value =
serde_json::from_str(&request_json).map_err(|e| js_err(e.to_string()))?;
let result = crate::services::tdlib::TDLIB_MANAGER
.send(request)
.await
.map_err(|e| js_err(e))?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
}
}),
))?;
}
{
let sc = skill_context.clone();
ops.set("tdlib_receive", Function::new(ctx.clone(),
Async(move |timeout_ms: u32| {
let skill_id = sc.skill_id.clone();
async move {
check_telegram_skill(&skill_id).map_err(|e| js_err(e))?;
let result = crate::services::tdlib::TDLIB_MANAGER.receive(timeout_ms).await;
if let Some(val) = result {
let json = serde_json::to_string(&val).map_err(|e| js_err(e.to_string()))?;
Ok::<Option<String>, rquickjs::Error>(Some(json))
} else {
Ok(None)
}
}
}),
))?;
}
{
let sc = skill_context.clone();
ops.set("tdlib_destroy", Function::new(ctx.clone(),
Async(move || {
let skill_id = sc.skill_id.clone();
async move {
check_telegram_skill(&skill_id).map_err(|e| js_err(e))?;
crate::services::tdlib::TDLIB_MANAGER.destroy().await.map_err(|e| js_err(e))
}
}),
))?;
}
// ========================================================================
// Model (4) - local LLM
// ========================================================================
ops.set("model_is_available", Function::new(ctx.clone(), || -> bool { false }))?;
ops.set("model_get_status", Function::new(ctx.clone(), || -> rquickjs::Result<String> {
let status = crate::services::llama::LLAMA_MANAGER.get_status();
serde_json::to_string(&status).map_err(|e| js_err(e.to_string()))
}))?;
ops.set("model_generate", Function::new(ctx.clone(),
Async(move |prompt: String, config_json: String| async move {
let config: crate::services::llama::GenerateConfig =
serde_json::from_str(&config_json).map_err(|e| js_err(e.to_string()))?;
crate::services::llama::LLAMA_MANAGER
.generate(&prompt, config)
.await
.map_err(|e| js_err(e))
}),
))?;
ops.set("model_summarize", Function::new(ctx.clone(),
Async(move |text: String, max_tokens: u32| async move {
crate::services::llama::LLAMA_MANAGER
.summarize(&text, max_tokens)
.await
.map_err(|e| js_err(e))
}),
))?;
// ========================================================================
// Register on globalThis
// ========================================================================
globals.set("__ops", ops)?;
Ok(())
}
+30 -30
View File
@@ -13,7 +13,7 @@ use std::sync::Arc;
/// Result of opening an IndexedDB database.
/// Used by the IndexedDB emulation layer for tdweb.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct IdbOpenResult {
/// Whether a version upgrade is needed.
pub needs_upgrade: bool,
@@ -29,7 +29,7 @@ pub struct IdbStorage {
/// Base directory for all databases.
data_dir: PathBuf,
/// Open database connections (db_name -> connection).
connections: Arc<RwLock<HashMap<String, Arc<RwLock<Connection>>>>>,
connections: Arc<RwLock<HashMap<String, Arc<parking_lot::Mutex<Connection>>>>>,
/// Database version info (used by IndexedDB emulation).
#[allow(dead_code)]
versions: Arc<RwLock<HashMap<String, u32>>>,
@@ -49,7 +49,7 @@ impl IdbStorage {
}
/// Get or create a database connection.
fn get_connection(&self, db_name: &str) -> Result<Arc<RwLock<Connection>>, String> {
fn get_connection(&self, db_name: &str) -> Result<Arc<parking_lot::Mutex<Connection>>, String> {
// Check if already open
if let Some(conn) = self.connections.read().get(db_name) {
return Ok(conn.clone());
@@ -81,7 +81,7 @@ impl IdbStorage {
)
.map_err(|e| format!("Failed to initialize database schema: {e}"))?;
let conn = Arc::new(RwLock::new(conn));
let conn = Arc::new(parking_lot::Mutex::new(conn));
self.connections
.write()
.insert(db_name.to_string(), conn.clone());
@@ -90,9 +90,9 @@ impl IdbStorage {
}
/// Open or create an IndexedDB database.
pub async fn open_database(&self, name: &str, version: u32) -> Result<IdbOpenResult, String> {
pub fn open_database(&self, name: &str, version: u32) -> Result<IdbOpenResult, String> {
let conn = self.get_connection(name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Get current version
let current_version: u32 = conn_guard
@@ -145,7 +145,7 @@ impl IdbStorage {
}
/// Delete a database.
pub async fn delete_database(&self, name: &str) -> Result<(), String> {
pub fn delete_database(&self, name: &str) -> Result<(), String> {
self.close_database(name);
let db_path = self.data_dir.join(format!("{}.sqlite", name));
if db_path.exists() {
@@ -164,7 +164,7 @@ impl IdbStorage {
auto_increment: bool,
) -> Result<(), String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Register the store
conn_guard
@@ -197,7 +197,7 @@ impl IdbStorage {
/// Delete an object store.
pub fn delete_object_store(&self, db_name: &str, store_name: &str) -> Result<(), String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Remove from registry
conn_guard
@@ -214,14 +214,14 @@ impl IdbStorage {
}
/// Get a value from an object store.
pub async fn get(
pub fn get(
&self,
db_name: &str,
store_name: &str,
key: &serde_json::Value,
) -> Result<Option<serde_json::Value>, String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
let key_str = serde_json::to_string(key).unwrap_or_else(|_| "null".to_string());
@@ -245,7 +245,7 @@ impl IdbStorage {
}
/// Put a value into an object store.
pub async fn put(
pub fn put(
&self,
db_name: &str,
store_name: &str,
@@ -253,7 +253,7 @@ impl IdbStorage {
value: &serde_json::Value,
) -> Result<(), String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
let key_str = serde_json::to_string(key).unwrap_or_else(|_| "null".to_string());
@@ -273,14 +273,14 @@ impl IdbStorage {
}
/// Delete a value from an object store.
pub async fn delete(
pub fn delete(
&self,
db_name: &str,
store_name: &str,
key: &serde_json::Value,
) -> Result<(), String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
let key_str = serde_json::to_string(key).unwrap_or_else(|_| "null".to_string());
@@ -296,9 +296,9 @@ impl IdbStorage {
}
/// Clear all values from an object store.
pub async fn clear(&self, db_name: &str, store_name: &str) -> Result<(), String> {
pub fn clear(&self, db_name: &str, store_name: &str) -> Result<(), String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
@@ -310,14 +310,14 @@ impl IdbStorage {
}
/// Get all values from an object store.
pub async fn get_all(
pub fn get_all(
&self,
db_name: &str,
store_name: &str,
count: Option<u32>,
) -> Result<Vec<serde_json::Value>, String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
let limit = count.map(|c| format!(" LIMIT {}", c)).unwrap_or_default();
@@ -337,14 +337,14 @@ impl IdbStorage {
}
/// Get all keys from an object store.
pub async fn get_all_keys(
pub fn get_all_keys(
&self,
db_name: &str,
store_name: &str,
count: Option<u32>,
) -> Result<Vec<serde_json::Value>, String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
let limit = count.map(|c| format!(" LIMIT {}", c)).unwrap_or_default();
@@ -364,9 +364,9 @@ impl IdbStorage {
}
/// Count values in an object store.
pub async fn count(&self, db_name: &str, store_name: &str) -> Result<u32, String> {
pub fn count(&self, db_name: &str, store_name: &str) -> Result<u32, String> {
let conn = self.get_connection(db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let table_name = format!("store_{}", sanitize_name(store_name));
@@ -394,7 +394,7 @@ impl IdbStorage {
) -> Result<usize, String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let params: Vec<Box<dyn rusqlite::ToSql>> = params
.iter()
@@ -418,7 +418,7 @@ impl IdbStorage {
) -> Result<serde_json::Value, String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let params: Vec<Box<dyn rusqlite::ToSql>> = params
.iter()
@@ -463,7 +463,7 @@ impl IdbStorage {
) -> Result<serde_json::Value, String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
let params: Vec<Box<dyn rusqlite::ToSql>> = params
.iter()
@@ -502,7 +502,7 @@ impl IdbStorage {
pub fn skill_kv_get(&self, skill_id: &str, key: &str) -> Result<serde_json::Value, String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Ensure KV table exists
conn_guard
@@ -535,7 +535,7 @@ impl IdbStorage {
) -> Result<(), String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Ensure KV table exists
conn_guard
@@ -580,7 +580,7 @@ impl IdbStorage {
pub fn skill_store_delete(&self, skill_id: &str, key: &str) -> Result<(), String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
conn_guard
.execute(
@@ -596,7 +596,7 @@ impl IdbStorage {
pub fn skill_store_keys(&self, skill_id: &str) -> Result<Vec<String>, String> {
let db_name = format!("skill_{}", skill_id);
let conn = self.get_connection(&db_name)?;
let conn_guard = conn.read();
let conn_guard = conn.lock();
// Ensure KV table exists
conn_guard