From 8eaf682d83cc59592d98b07919dfa2d4903d9147 Mon Sep 17 00:00:00 2001 From: cyrus Date: Tue, 17 Mar 2026 23:09:37 +0530 Subject: [PATCH] remove: deprecated QuickJS runtime and associated libraries - Deleted `bootstrap.js`, `qjs_ops`, and `mod.rs` from QuickJS runtime module. - Removed unused QuickJS-related code, including APIs for browser-like shims, IndexedDB, WebSocket, TDLib, and OAuth. - Simplified project structure by eliminating redundant QuickJS dependencies. --- src-tauri/src/runtime/qjs_skill_instance.rs | 4 +- .../src/services/quickjs-libs/bootstrap.js | 1058 ----------------- src-tauri/src/services/quickjs-libs/mod.rs | 13 - .../src/services/quickjs-libs/qjs_ops/mod.rs | 48 - .../services/quickjs-libs/qjs_ops/ops_core.rs | 116 -- .../services/quickjs-libs/qjs_ops/ops_net.rs | 183 --- .../quickjs-libs/qjs_ops/ops_state.rs | 85 -- .../quickjs-libs/qjs_ops/ops_storage.rs | 260 ---- .../services/quickjs-libs/qjs_ops/types.rs | 161 --- .../src/services/quickjs-libs/service.rs | 457 ------- .../src/services/quickjs-libs/storage.rs | 673 ----------- 11 files changed, 2 insertions(+), 3056 deletions(-) delete mode 100644 src-tauri/src/services/quickjs-libs/bootstrap.js delete mode 100644 src-tauri/src/services/quickjs-libs/mod.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/mod.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/ops_storage.rs delete mode 100644 src-tauri/src/services/quickjs-libs/qjs_ops/types.rs delete mode 100644 src-tauri/src/services/quickjs-libs/service.rs delete mode 100644 src-tauri/src/services/quickjs-libs/storage.rs diff --git a/src-tauri/src/runtime/qjs_skill_instance.rs b/src-tauri/src/runtime/qjs_skill_instance.rs index 5a00717f2..e4766fce7 100644 --- a/src-tauri/src/runtime/qjs_skill_instance.rs +++ b/src-tauri/src/runtime/qjs_skill_instance.rs @@ -191,7 +191,7 @@ impl QjsSkillInstance { } // Load bootstrap - let bootstrap_code = include_str!("../services/quickjs-libs/bootstrap.js"); + let bootstrap_code = include_str!("../services/quickjs_libs/bootstrap.js"); if let Err(e) = js_ctx.eval::(bootstrap_code) { let detail = format_js_exception(&js_ctx, &e); return Err(format!("Bootstrap failed: {detail}")); @@ -383,7 +383,7 @@ async fn run_event_loop( let new_map: HashMap = ops .data .iter() - .map(|(k, v)| (k.clone(), v.clone())) + .map(|(k, v): (&String, &serde_json::Value)| (k.clone(), v.clone())) .collect(); state.write().published_state = new_map.clone(); diff --git a/src-tauri/src/services/quickjs-libs/bootstrap.js b/src-tauri/src/services/quickjs-libs/bootstrap.js deleted file mode 100644 index 839abca3a..000000000 --- a/src-tauri/src/services/quickjs-libs/bootstrap.js +++ /dev/null @@ -1,1058 +0,0 @@ -/** - * Bootstrap JavaScript for QuickJS Runtime - * - * Provides browser-like API shims that tdweb expects. - * These shims call Rust "ops" for actual I/O. - */ - -// Make globalThis.self and globalThis.window point to globalThis -globalThis.self = globalThis; -globalThis.window = globalThis; - -// ============================================================================ -// Console (uses Rust logging) -// ============================================================================ -globalThis.console = { - log: function (...args) { - __ops.console_log(args.map(String).join(' ')); - }, - info: function (...args) { - __ops.console_log(args.map(String).join(' ')); - }, - warn: function (...args) { - __ops.console_warn(args.map(String).join(' ')); - }, - error: function (...args) { - __ops.console_error(args.map(String).join(' ')); - }, - debug: function (...args) { - __ops.console_log('[DEBUG] ' + args.map(String).join(' ')); - }, -}; - -// ============================================================================ -// Timers (setTimeout, setInterval, clearTimeout, clearInterval) -// ============================================================================ -const timerCallbacks = new Map(); -let nextTimerId = 1; - -globalThis.setTimeout = function (callback, delay, ...args) { - const id = nextTimerId++; - timerCallbacks.set(id, { callback, args, type: 'timeout' }); - __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' }); - __ops.timer_start(id, delay || 0, true); - return id; -}; - -globalThis.clearTimeout = function (id) { - timerCallbacks.delete(id); - __ops.timer_cancel(id); -}; - -globalThis.clearInterval = function (id) { - timerCallbacks.delete(id); - __ops.timer_cancel(id); -}; - -// Timer callback handler (called from Rust) -globalThis.__handleTimer = function (id) { - const timer = timerCallbacks.get(id); - if (timer) { - if (timer.type === 'timeout') { - timerCallbacks.delete(id); - } - try { - timer.callback.apply(null, timer.args); - } catch (e) { - console.error('Timer callback error:', e); - } - } -}; - -// ============================================================================ -// AbortController / AbortSignal Polyfill -// ============================================================================ -class AbortSignal { - constructor() { - this.aborted = false; - this.reason = undefined; - this._listeners = []; - } - - addEventListener(type, listener) { - if (type === 'abort') { - this._listeners.push(listener); - } - } - - removeEventListener(type, listener) { - if (type === 'abort') { - const idx = this._listeners.indexOf(listener); - if (idx >= 0) this._listeners.splice(idx, 1); - } - } - - throwIfAborted() { - if (this.aborted) { - throw this.reason || new Error('Aborted'); - } - } -} - -class AbortController { - constructor() { - this.signal = new AbortSignal(); - } - - abort(reason) { - if (!this.signal.aborted) { - this.signal.aborted = true; - this.signal.reason = reason || new Error('Aborted'); - for (const listener of this.signal._listeners) { - try { - listener({ type: 'abort', target: this.signal }); - } catch (e) { - console.error('AbortController listener error:', e); - } - } - } - } -} - -globalThis.AbortController = AbortController; -globalThis.AbortSignal = AbortSignal; - -// ============================================================================ -// Fetch API -// ============================================================================ -globalThis.fetch = async function (url, options = {}) { - const method = options.method || 'GET'; - const headers = options.headers || {}; - const body = options.body || null; - - // Convert Headers object to plain object if needed - let headersObj = {}; - if (headers instanceof Headers) { - headers.forEach((value, key) => { - headersObj[key] = value; - }); - } else { - headersObj = headers; - } - - // __ops.fetch expects a JSON string for options (not a JS object) - const resultJson = await __ops.fetch(url.toString(), JSON.stringify({ - method, - headers: headersObj, - body: typeof body === 'string' ? body : body ? JSON.stringify(body) : null, - })); - - // __ops.fetch returns a JSON string — parse it to access status/headers/body - const parsed = JSON.parse(resultJson); - - return new Response(parsed.body, { - status: parsed.status, - statusText: parsed.statusText || '', - headers: new Headers(parsed.headers || {}), - }); -}; - -// Response class for fetch -class Response { - constructor(body, init = {}) { - this._body = body; - this.status = init.status || 200; - this.statusText = init.statusText || ''; - this.headers = init.headers || new Headers(); - this.ok = this.status >= 200 && this.status < 300; - } - - async text() { - return this._body; - } - - async json() { - return JSON.parse(this._body); - } - - async arrayBuffer() { - // Convert string to ArrayBuffer - const encoder = new TextEncoder(); - return encoder.encode(this._body).buffer; - } - - async blob() { - throw new Error('Blob not supported'); - } -} - -// Headers class for fetch -class Headers { - constructor(init = {}) { - this._headers = {}; - if (init) { - if (typeof init.forEach === 'function') { - init.forEach((value, key) => { - this._headers[key.toLowerCase()] = value; - }); - } else { - Object.entries(init).forEach(([key, value]) => { - this._headers[key.toLowerCase()] = value; - }); - } - } - } - - get(name) { - return this._headers[name.toLowerCase()] || null; - } - - set(name, value) { - this._headers[name.toLowerCase()] = value; - } - - has(name) { - return name.toLowerCase() in this._headers; - } - - delete(name) { - delete this._headers[name.toLowerCase()]; - } - - forEach(callback) { - Object.entries(this._headers).forEach(([key, value]) => { - callback(value, key, this); - }); - } -} - -globalThis.Response = Response; -globalThis.Headers = Headers; - -// ============================================================================ -// WebSocket API -// ============================================================================ -class WebSocket { - static CONNECTING = 0; - static OPEN = 1; - static CLOSING = 2; - static CLOSED = 3; - - constructor(url, protocols) { - this.url = url; - this.protocols = protocols; - this.readyState = WebSocket.CONNECTING; - this.binaryType = 'blob'; - this._id = null; - - // Event handlers - this.onopen = null; - this.onclose = null; - this.onerror = null; - this.onmessage = null; - - // Connect asynchronously - this._connect(); - } - - async _connect() { - try { - this._id = await __ops.ws_connect(this.url); - this.readyState = WebSocket.OPEN; - - // Register for message callbacks - WebSocket._instances.set(this._id, this); - - if (this.onopen) { - this.onopen({ type: 'open', target: this }); - } - - // Start receiving messages - this._startReceiving(); - } catch (e) { - this.readyState = WebSocket.CLOSED; - if (this.onerror) { - this.onerror({ type: 'error', error: e, target: this }); - } - } - } - - async _startReceiving() { - while (this.readyState === WebSocket.OPEN) { - try { - const message = await __ops.ws_recv(this._id); - if (message === null) { - // Connection closed - this._handleClose(1000, ''); - break; - } - - if (this.onmessage) { - this.onmessage({ type: 'message', data: message, target: this }); - } - } catch (e) { - if (this.readyState === WebSocket.OPEN) { - this._handleClose(1006, e.toString()); - } - break; - } - } - } - - _handleClose(code, reason) { - this.readyState = WebSocket.CLOSED; - WebSocket._instances.delete(this._id); - - if (this.onclose) { - this.onclose({ - type: 'close', - code, - reason, - wasClean: code === 1000, - target: this, - }); - } - } - - send(data) { - if (this.readyState !== WebSocket.OPEN) { - throw new Error('WebSocket is not open'); - } - - const dataStr = typeof data === 'string' ? data : JSON.stringify(data); - __ops.ws_send(this._id, dataStr); - } - - close(code = 1000, reason = '') { - if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) { - return; - } - - this.readyState = WebSocket.CLOSING; - __ops.ws_close(this._id, code, reason); - } -} - -WebSocket._instances = new Map(); -globalThis.WebSocket = WebSocket; - -// ============================================================================ -// IndexedDB API (for tdweb persistence) -// ============================================================================ -class IDBFactory { - open(name, version = 1) { - return new IDBOpenDBRequest(name, version); - } - - deleteDatabase(name) { - const request = new IDBRequest(); - (async () => { - try { - await __ops.idb_delete_database(name); - request._success(undefined); - } catch (e) { - request._error(e); - } - })(); - return request; - } -} - -class IDBRequest { - constructor() { - this.result = undefined; - this.error = null; - this.readyState = 'pending'; - this.onsuccess = null; - this.onerror = null; - } - - _success(result) { - this.result = result; - this.readyState = 'done'; - if (this.onsuccess) { - this.onsuccess({ type: 'success', target: this }); - } - } - - _error(error) { - this.error = error; - this.readyState = 'done'; - if (this.onerror) { - this.onerror({ type: 'error', target: this }); - } - } -} - -class IDBOpenDBRequest extends IDBRequest { - constructor(name, version) { - super(); - this.onupgradeneeded = null; - this.onblocked = null; - this._name = name; - this._version = version; - this._open(); - } - - async _open() { - try { - const info = await __ops.idb_open(this._name, this._version); - const db = new IDBDatabase(this._name, this._version, info.objectStores || []); - - if (info.needsUpgrade) { - if (this.onupgradeneeded) { - const event = { - type: 'upgradeneeded', - target: this, - oldVersion: info.oldVersion || 0, - newVersion: this._version, - }; - // Temporarily set result for upgrade - this.result = db; - this.onupgradeneeded(event); - } - } - - this._success(db); - } catch (e) { - this._error(e); - } - } -} - -class IDBDatabase { - constructor(name, version, objectStoreNames) { - this.name = name; - this.version = version; - this.objectStoreNames = objectStoreNames; - this.onclose = null; - this.onerror = null; - this.onversionchange = null; - } - - createObjectStore(name, options = {}) { - __ops.idb_create_object_store(this.name, name, options); - this.objectStoreNames.push(name); - return new IDBObjectStore(this, name); - } - - deleteObjectStore(name) { - __ops.idb_delete_object_store(this.name, name); - const idx = this.objectStoreNames.indexOf(name); - if (idx >= 0) this.objectStoreNames.splice(idx, 1); - } - - transaction(storeNames, mode = 'readonly') { - return new IDBTransaction(this, storeNames, mode); - } - - close() { - __ops.idb_close(this.name); - } -} - -class IDBTransaction { - constructor(db, storeNames, mode) { - this.db = db; - this.mode = mode; - this.error = null; - this.oncomplete = null; - this.onerror = null; - this.onabort = null; - this._storeNames = Array.isArray(storeNames) ? storeNames : [storeNames]; - } - - objectStore(name) { - if (!this._storeNames.includes(name)) { - throw new Error(`Object store "${name}" not in transaction scope`); - } - return new IDBObjectStore(this.db, name, this); - } - - abort() { - if (this.onabort) { - this.onabort({ type: 'abort', target: this }); - } - } - - _complete() { - if (this.oncomplete) { - this.oncomplete({ type: 'complete', target: this }); - } - } -} - -class IDBObjectStore { - constructor(db, name, transaction = null) { - this._db = db; - this.name = name; - this._transaction = transaction; - } - - get(key) { - const request = new IDBRequest(); - (async () => { - try { - const value = await __ops.idb_get(this._db.name, this.name, key); - request._success(value); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - put(value, key) { - const request = new IDBRequest(); - (async () => { - try { - await __ops.idb_put(this._db.name, this.name, key, value); - request._success(key); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - add(value, key) { - return this.put(value, key); - } - - delete(key) { - const request = new IDBRequest(); - (async () => { - try { - await __ops.idb_delete(this._db.name, this.name, key); - request._success(undefined); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - clear() { - const request = new IDBRequest(); - (async () => { - try { - await __ops.idb_clear(this._db.name, this.name); - request._success(undefined); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - getAll(query, count) { - const request = new IDBRequest(); - (async () => { - try { - const values = await __ops.idb_get_all(this._db.name, this.name, count); - request._success(values); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - getAllKeys(query, count) { - const request = new IDBRequest(); - (async () => { - try { - const keys = await __ops.idb_get_all_keys(this._db.name, this.name, count); - request._success(keys); - } catch (e) { - request._error(e); - } - })(); - return request; - } - - count() { - const request = new IDBRequest(); - (async () => { - try { - const count = await __ops.idb_count(this._db.name, this.name); - request._success(count); - } catch (e) { - request._error(e); - } - })(); - return request; - } -} - -globalThis.indexedDB = new IDBFactory(); -globalThis.IDBFactory = IDBFactory; -globalThis.IDBRequest = IDBRequest; -globalThis.IDBOpenDBRequest = IDBOpenDBRequest; -globalThis.IDBDatabase = IDBDatabase; -globalThis.IDBTransaction = IDBTransaction; -globalThis.IDBObjectStore = IDBObjectStore; - -// ============================================================================ -// TextEncoder / TextDecoder (for binary data handling) -// ============================================================================ -if (typeof globalThis.TextEncoder === 'undefined') { - globalThis.TextEncoder = class TextEncoder { - encode(str) { - const arr = []; - for (let i = 0; i < str.length; i++) { - let c = str.charCodeAt(i); - if (c < 128) { - arr.push(c); - } else if (c < 2048) { - arr.push((c >> 6) | 192, (c & 63) | 128); - } else { - arr.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); - } - } - return new Uint8Array(arr); - } - }; -} - -if (typeof globalThis.TextDecoder === 'undefined') { - globalThis.TextDecoder = class TextDecoder { - decode(arr) { - if (!arr) return ''; - const bytes = arr instanceof Uint8Array ? arr : new Uint8Array(arr); - let result = ''; - for (let i = 0; i < bytes.length; i++) { - result += String.fromCharCode(bytes[i]); - } - return result; - } - }; -} - -// ============================================================================ -// atob / btoa (Base64) -// ============================================================================ -if (typeof globalThis.atob === 'undefined') { - globalThis.atob = function (str) { - return __ops.atob(str); - }; -} - -if (typeof globalThis.btoa === 'undefined') { - globalThis.btoa = function (str) { - return __ops.btoa(str); - }; -} - -// ============================================================================ -// Crypto API (basic) -// ============================================================================ -globalThis.crypto = { - getRandomValues: function (array) { - const bytes = __ops.crypto_random(array.length); - array.set(bytes); - return array; - }, -}; - -// ============================================================================ -// Performance API (basic) -// ============================================================================ -globalThis.performance = { - now: function () { - return __ops.performance_now(); - }, -}; - -// ============================================================================ -// Skill Bridge API -// ============================================================================ -// These are exposed to skills via the `platform`, `db`, `state`, etc globals - -globalThis.__db = { - exec: function (sql, paramsJson) { - return __ops.db_exec(sql, paramsJson); - }, - get: function (sql, paramsJson) { - return __ops.db_get(sql, paramsJson); - }, - all: function (sql, paramsJson) { - return __ops.db_all(sql, paramsJson); - }, - kvGet: function (key) { - return __ops.db_kv_get(key); - }, - kvSet: function (key, valueJson) { - return __ops.db_kv_set(key, valueJson); - }, -}; - - -globalThis.__platform = { - os: function () { - return __ops.platform_os(); - }, - env: function (key) { - return __ops.platform_env(key); - }, -}; - -// High-level wrappers for skills (QuickJS bridge) -globalThis.db = { - exec: function (sql, params) { - return __db.exec(sql, params ? JSON.stringify(params) : undefined); - }, - get: function (sql, params) { - var result = __db.get(sql, params ? JSON.stringify(params) : undefined); - return JSON.parse(result); - }, - all: function (sql, params) { - var result = __db.all(sql, params ? JSON.stringify(params) : undefined); - return JSON.parse(result); - }, - kvGet: function (key) { - var result = __db.kvGet(key); - return JSON.parse(result); - }, - kvSet: function (key, value) { - return __db.kvSet(key, JSON.stringify(value)); - }, -}; - -globalThis.net = { - fetch: async function (url, options) { - const result = await __ops.fetch(url, options ? JSON.stringify(options) : '{}'); - return JSON.parse(result); - }, -}; - -globalThis.platform = { - os: function () { - return __platform.os(); - }, - env: function (key) { - return __platform.env(key); - }, -}; - -// ============================================================================ -// State Bridge API (for skills to publish state) -// ============================================================================ -globalThis.__state = { - get: function (key) { - return __ops.state_get(key); - }, - set: function (key, valueJson) { - return __ops.state_set(key, valueJson); - }, - setPartial: function (partialJson) { - return __ops.state_set_partial(partialJson); - }, -}; - -globalThis.state = { - get: function (key) { - var result = __ops.store_get(key); - return JSON.parse(result); - }, - set: function (key, value) { - __ops.store_set(key, JSON.stringify(value)); - __state.set(key, JSON.stringify(value)); - }, - setPartial: function (partial) { - var keys = Object.keys(partial); - for (var i = 0; i < keys.length; i++) { - __ops.store_set(keys[i], JSON.stringify(partial[keys[i]])); - } - __state.setPartial(JSON.stringify(partial)); - }, - delete: function (key) { - return __ops.store_delete(key); - }, - keys: function () { - var result = __ops.store_keys(); - return JSON.parse(result); - }, -}; - -// ============================================================================ -// Data Bridge API (for skills to read/write files) -// ============================================================================ -globalThis.__data = { - read: function (filename) { - return __ops.data_read(filename); - }, - write: function (filename, content) { - return __ops.data_write(filename, content); - }, -}; - -globalThis.data = { - read: function (filename) { - return __data.read(filename); - }, - write: function (filename, content) { - return __data.write(filename, content); - }, -}; - -// ============================================================================ -// OAuth Bridge API (credential management and authenticated proxy) -// ============================================================================ -(function () { - globalThis.__oauthCredential = null; - - globalThis.oauth = { - /** Get the current OAuth credential, or null if not connected. */ - getCredential: function () { - return globalThis.__oauthCredential; - }, - - /** - * Make an authenticated API request proxied through the backend. - * Path is relative to manifest's apiBaseUrl. - */ - fetch: async function (path, options) { - if (!globalThis.__oauthCredential) { - return { - status: 401, - headers: {}, - body: JSON.stringify({ error: 'No OAuth credential. Complete OAuth setup first.' }), - }; - } - var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz'; - var jwtToken = __ops.get_session_token() || ''; - var cleanPath = path.charAt(0) === '/' ? path.slice(1) : path; - var proxyUrl = backendUrl + '/proxy/by-id/' + globalThis.__oauthCredential.credentialId + '/' + cleanPath; - var method = (options && options.method) || 'GET'; - var headers = { 'Content-Type': 'application/json' }; - if (jwtToken) { - headers['Authorization'] = 'Bearer ' + jwtToken; - } - if (options && options.headers) { - for (var k in options.headers) { - headers[k] = options.headers[k]; - } - } - var fetchOpts = { - method: method, - headers: headers, - body: options ? options.body : undefined, - timeout: options ? options.timeout : undefined, - }; - - var result = await net.fetch(proxyUrl, fetchOpts); - return result; - }, - - /** Revoke the current OAuth credential server-side. */ - revoke: async function () { - if (__oauthCredential) { - try { - var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz'; - var jwtToken = __ops.get_session_token() || ''; - var revokeOpts = JSON.stringify({ - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + jwtToken, - }, - }); - await net.fetch( - backendUrl + '/auth/integrations/' + __oauthCredential.credentialId, - revokeOpts - ); - } catch (e) { - /* best effort */ - } - } - __oauthCredential = null; - return true; - }, - - /** Internal: set credential (called by runtime on oauth/complete). */ - __setCredential: function (cred) { - __oauthCredential = cred; - }, - }; -})(); - -// ============================================================================ -// Tools array (skills assign to this global) -// ============================================================================ -globalThis.tools = []; - -// ============================================================================ -// Cron Bridge API (placeholder - requires integration with CronScheduler) -// ============================================================================ -globalThis.cron = { - register: function (scheduleId, cronExpr) { - console.warn('[cron] register not implemented in QuickJS runtime yet'); - return false; - }, - unregister: function (scheduleId) { - console.warn('[cron] unregister not implemented in QuickJS runtime yet'); - return false; - }, - list: function () { - console.warn('[cron] list not implemented in QuickJS runtime yet'); - return []; - }, -}; - -// ============================================================================ -// Skills Bridge API (placeholder - requires integration with SkillRegistry) -// ============================================================================ -globalThis.skills = { - list: function () { - console.warn('[skills] list not implemented in QuickJS runtime yet'); - return []; - }, - callTool: function (skillId, toolName, args) { - console.warn('[skills] callTool not implemented in QuickJS runtime yet'); - return { error: 'Not implemented' }; - }, -}; - -// ============================================================================ -// TDLib Bridge API (telegram skill only) -// ============================================================================ -// Provides native TDLib access for the telegram skill. -// This is only available on desktop - Android uses Tauri invoke() instead. - -globalThis.tdlib = { - /** - * Check if TDLib ops are available. - * @returns {boolean} True on desktop, false on mobile/web. - */ - isAvailable: function () { - try { - return typeof __ops?.tdlib_is_available === 'function' - ? __ops.tdlib_is_available() - : false; - } catch (e) { - return false; - } - }, - - /** - * Create client and set TDLib parameters in a single atomic call. - * API credentials are stored on the Rust side only. - * @param {string} dataDir - Path to store TDLib data files. - * @returns {Promise} Client ID. - */ - ensureInitialized: async function (dataDir) { - return await __ops.tdlib_ensure_initialized(dataDir); - }, - - /** - * Create a TDLib client with the given data directory. - * @param {string} dataDir - Path to store TDLib data files. - * @returns {Promise} Client ID (always 1 for singleton). - */ - createClient: function (dataDir) { - return __ops.tdlib_create_client(dataDir); - }, - - /** - * Send a TDLib request and wait for the response. - * @param {string} requestJson - JSON-serialized TDLib API request with @type field. - * @returns {Promise} JSON-serialized TDLib response. - */ - send: async function (requestJson) { - return await __ops.tdlib_send(requestJson); - }, - - /** - * Receive the next TDLib update (with timeout). - * @param {number} [timeoutMs=1000] - Timeout in milliseconds. - * @returns {Promise} Update object or null if timeout. - */ - receive: async function (timeoutMs = 1000) { - const resultJson = await __ops.tdlib_receive(timeoutMs); - return resultJson ? JSON.parse(resultJson) : null; - }, - - /** - * Destroy the TDLib client and clean up resources. - * @returns {Promise} - */ - destroy: async function () { - return await __ops.tdlib_destroy(); - }, -}; - -// ============================================================================ -// Model Bridge API (routes to cloud backend) -// ============================================================================ - -globalThis.model = { - /** - * Generate text from a prompt via the backend API. - * @param {string} prompt - Input prompt - * @param {object} [options] - Generation options - * @param {number} [options.maxTokens=2048] - Max output tokens - * @param {number} [options.temperature=0.7] - Sampling temperature - * @returns {string} - */ - generate: async function (prompt, options) { - var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz'; - var jwtToken = __ops.get_session_token() || ''; - var body = { prompt: prompt }; - if (options && options.maxTokens) body.maxTokens = options.maxTokens; - if (options && options.temperature) body.temperature = options.temperature; - var result = await net.fetch(backendUrl + '/api/ai/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + jwtToken, - }, - body: JSON.stringify(body), - timeout: 30000, - }); - var parsed = JSON.parse(result); - if (parsed.status >= 400) { - throw new Error('Backend returned ' + parsed.status + ': ' + parsed.body); - } - var data = JSON.parse(parsed.body); - return data.text || ''; - }, - - /** - * Summarize text via the backend API. - * @param {string} text - Text to summarize - * @param {object} [options] - Options - * @param {number} [options.maxTokens=500] - Target summary length - * @returns {string} - */ - summarize: async function (text, options) { - var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz'; - var jwtToken = __ops.get_session_token() || ''; - var body = { text: text }; - if (options && options.maxTokens) body.maxTokens = options.maxTokens; - var result = await net.fetch(backendUrl + '/api/ai/summarize', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + jwtToken, - }, - body: JSON.stringify(body), - timeout: 30000, - }); - var parsed = JSON.parse(result); - if (parsed.status >= 400) { - throw new Error('Backend returned ' + parsed.status + ': ' + parsed.body); - } - var data = JSON.parse(parsed.body); - return data.summary || ''; - }, -}; - -console.log('[bootstrap] Model API initialized'); -console.log('[bootstrap] QuickJS browser APIs initialized'); diff --git a/src-tauri/src/services/quickjs-libs/mod.rs b/src-tauri/src/services/quickjs-libs/mod.rs deleted file mode 100644 index aa590cdfb..000000000 --- a/src-tauri/src/services/quickjs-libs/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! TDLib Runtime Module -//! -//! 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 qjs_ops; -pub mod service; -pub mod storage; - -#[allow(unused_imports)] -pub use service::{TdClientAdapter, TdClientConfig, TdUpdate, TdlibV8Service}; -pub use storage::IdbStorage; diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/mod.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/mod.rs deleted file mode 100644 index 2acadac04..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! QuickJS native ops registered on `globalThis.__ops`. -//! -//! Split by category for readability: -//! - `types` — shared state structs, constants, helpers -//! - `ops_core` — console, crypto, performance, platform, timers -//! - `ops_net` — fetch, WebSocket, net bridge -//! - `ops_storage` — IndexedDB, DB bridge, Store bridge -//! - `ops_state` — published state, filesystem data -//! - `ops_tdlib` — TDLib (Telegram) integration - -mod ops_core; -mod ops_net; -mod ops_state; -mod ops_storage; -mod ops_tdlib; -pub mod types; - -// Re-export public API used by qjs_skill_instance.rs -pub use types::{poll_timers, SkillContext, SkillState, TimerState, WebSocketState}; - -use parking_lot::RwLock; -use rquickjs::{Ctx, Object, Result as JsResult}; -use std::sync::Arc; - -use crate::services::quickjs_libs::storage::IdbStorage; -use types::SkillContext as SC; - -/// Register all ops on `globalThis.__ops`. -pub fn register_ops( - ctx: &Ctx<'_>, - storage: IdbStorage, - skill_context: SC, - skill_state: Arc>, - timer_state: Arc>, - ws_state: Arc>, -) -> JsResult<()> { - let globals = ctx.globals(); - let ops = Object::new(ctx.clone())?; - - ops_core::register(ctx, &ops, timer_state)?; - ops_net::register(ctx, &ops, ws_state)?; - ops_storage::register(ctx, &ops, storage, skill_context.clone())?; - ops_state::register(ctx, &ops, skill_state, skill_context.clone())?; - ops_tdlib::register(ctx, &ops, skill_context.clone())?; - - globals.set("__ops", ops)?; - Ok(()) -} diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs deleted file mode 100644 index 1cd669920..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Core ops: console, crypto, performance, platform, timers. - -use parking_lot::RwLock; -use rquickjs::{Ctx, Function, Object}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use super::types::{TimerEntry, TimerState, ALLOWED_ENV_VARS}; - -pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, timer_state: Arc>) -> rquickjs::Result<()> { - // ======================================================================== - // 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 { - 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 { - use base64::Engine; - let bytes = base64::engine::general_purpose::STANDARD - .decode(&input) - .map_err(|e| super::types::js_err(e.to_string()))?; - String::from_utf8(bytes).map_err(|e| super::types::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 { - if ALLOWED_ENV_VARS.contains(&key.as_str()) { - std::env::var(&key).ok() - } else { - None - } - }))?; - - ops.set("get_session_token", Function::new(ctx.clone(), || -> String { - let token = crate::commands::auth::SESSION_SERVICE.get_token().unwrap_or_default(); - return token; - }))?; - - // ======================================================================== - // 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; - ops.set("timer_cancel", Function::new(ctx.clone(), move |id: u32| { - let mut state = ts.write(); - state.timers.remove(&id); - }))?; - } - - Ok(()) -} diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs deleted file mode 100644 index a8f038210..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Network ops: fetch, WebSocket, net bridge. - -use parking_lot::RwLock; -use rquickjs::{function::Async, Ctx, Function, Object}; -use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; - -use super::types::{js_err, WebSocketConnection, WebSocketState}; - -/// Shared HTTP client — built once, reused across all fetch calls. -/// Using a shared client enables connection pooling, persistent TLS sessions, -/// and prevents per-request TLS handshake overhead that can cause hangs. -static HTTP_CLIENT: OnceLock = OnceLock::new(); - -fn get_http_client() -> &'static reqwest::Client { - HTTP_CLIENT.get_or_init(|| { - reqwest::Client::builder() - .use_rustls_tls() - .connect_timeout(std::time::Duration::from_secs(10)) - .pool_idle_timeout(std::time::Duration::from_secs(90)) - .pool_max_idle_per_host(10) - .build() - .expect("failed to build shared HTTP client") - }) -} - -pub fn register<'js>( - ctx: &Ctx<'js>, - ops: &Object<'js>, - ws_state: Arc>, -) -> rquickjs::Result<()> { - // ======================================================================== - // 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 timeout_secs = opts["timeout"] - .as_u64() - .or_else(|| opts["timeout"].as_f64().map(|f| f as u64)) - .unwrap_or(30); - - let client = get_http_client(); - let mut req = match method { - "GET" => client.get(&url), - "POST" => client.post(&url), - "PUT" => client.put(&url), - "PATCH" => client.patch(&url), - "DELETE" => client.delete(&url), - _ => client.get(&url), - }; - - req = req.timeout(std::time::Duration::from_secs(timeout_secs)); - - 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()); - } - - // Hard safety net: tokio timeout wraps send+body read so a stalled - // connection cannot block the QuickJS event loop indefinitely even - // if the per-request timeout on the reqwest builder fails to fire. - let total_deadline = std::time::Duration::from_secs(timeout_secs + 5); - let response = tokio::time::timeout(total_deadline, req.send()) - .await - .map_err(|_| js_err(format!("request timed out after {}s", timeout_secs + 5)))? - .map_err(|e| { - let mut msg = e.to_string(); - let mut source = std::error::Error::source(&e); - while let Some(cause) = source { - msg.push_str(&format!(" | caused by: {cause}")); - source = std::error::Error::source(cause); - } - js_err(msg) - })?; - - let status = response.status().as_u16(); - let status_text = response - .status() - .canonical_reason() - .unwrap_or("") - .to_string(); - let headers: HashMap = response - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - let body_text = tokio::time::timeout( - std::time::Duration::from_secs(timeout_secs + 5), - response.text(), - ) - .await - .map_err(|_| js_err(format!("body read timed out after {}s", timeout_secs + 5)))? - .map_err(|e| js_err(e.to_string()))?; - - let result = serde_json::json!({ - "status": status, - "statusText": status_text, - "headers": headers, - "body": body_text, - }); - - Ok::(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::(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::, rquickjs::Error>(None) } - }), - ), - )?; - } - - { - let ws = ws_state; - ops.set( - "ws_close", - Function::new(ctx.clone(), move |id: u32, _code: u16, _reason: String| { - let mut state = ws.write(); - state.connections.remove(&id); - }), - )?; - } - Ok(()) -} diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs deleted file mode 100644 index ddf1b892a..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! State and data ops: published state get/set, filesystem data read/write. - -use parking_lot::RwLock; -use rquickjs::{Ctx, Function, Object}; -use std::sync::Arc; - -use super::types::{js_err, SkillContext, SkillState}; - -pub fn register<'js>( - ctx: &Ctx<'js>, - ops: &Object<'js>, - skill_state: Arc>, - skill_context: SkillContext, -) -> rquickjs::Result<()> { - // ======================================================================== - // State Bridge (3) - // ======================================================================== - - { - let ss = skill_state.clone(); - ops.set("state_get", Function::new(ctx.clone(), - move |key: String| -> rquickjs::Result { - 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); - state.dirty = true; - Ok(()) - }, - ))?; - } - - { - let ss = skill_state; - ops.set("state_set_partial", Function::new(ctx.clone(), - move |partial_json: String| -> rquickjs::Result<()> { - let partial: serde_json::Map = - 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); - } - state.dirty = true; - Ok(()) - }, - ))?; - } - - // ======================================================================== - // Data Bridge (2) - // ======================================================================== - - { - let sc = skill_context.clone(); - ops.set("data_read", Function::new(ctx.clone(), - move |filename: String| -> rquickjs::Result { - 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; - 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())) - }, - ))?; - } - - Ok(()) -} diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_storage.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_storage.rs deleted file mode 100644 index 69f21c67b..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_storage.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Storage ops: IndexedDB, DB bridge, Store bridge. - -use rquickjs::{Ctx, Function, Object}; - -use super::types::{js_err, SkillContext}; -use crate::services::quickjs_libs::storage::IdbStorage; - -pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, storage: IdbStorage, skill_context: SkillContext) -> rquickjs::Result<()> { - // ======================================================================== - // IndexedDB (11) - all sync - // ======================================================================== - - { - let s = storage.clone(); - ops.set("idb_open", Function::new(ctx.clone(), - move |name: String, version: u32| -> rquickjs::Result { - 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 { - 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| -> rquickjs::Result { - 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| -> rquickjs::Result { - 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 { - 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| -> rquickjs::Result { - let params: Vec = 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, ¶ms).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| -> rquickjs::Result { - let params: Vec = 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, ¶ms).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| -> rquickjs::Result { - let params: Vec = 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, ¶ms).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 { - 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 { - 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; - let sc = skill_context; - ops.set("store_keys", Function::new(ctx.clone(), - move || -> rquickjs::Result { - 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())) - }, - ))?; - } - - Ok(()) -} diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs deleted file mode 100644 index 3113f53f1..000000000 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Shared types, state structs, and helpers for QuickJS ops. - -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::time::{Duration, Instant}; - -// ============================================================================ -// 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, -} - -impl TimerState { - pub fn poll_ready(&mut self) -> Vec { - 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 { - let now = Instant::now(); - self.timers - .values() - .map(|e| e.deadline.saturating_duration_since(now)) - .min() - } -} - -pub fn poll_timers(timer_state: &RwLock) -> (Vec, Option) { - 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, - /// Set to true when data is modified; the event loop clears it after syncing. - #[serde(skip)] - pub dirty: bool, -} - -impl Default for SkillState { - fn default() -> Self { - Self { - data: serde_json::Map::new(), - dirty: false, - } - } -} - -// ============================================================================ -// WebSocket State (placeholder) -// ============================================================================ - -#[derive(Debug)] -pub struct WebSocketConnection { - pub url: String, -} - -#[derive(Debug, Default)] -pub struct WebSocketState { - pub connections: HashMap, - pub next_id: u32, -} - -// ============================================================================ -// Constants & Helpers -// ============================================================================ - -pub const ALLOWED_ENV_VARS: &[&str] = &[ - "VITE_BACKEND_URL", - "BACKEND_URL", - "JWT_TOKEN", - "VITE_TELEGRAM_BOT_USERNAME", - "VITE_TELEGRAM_BOT_ID", - "NODE_ENV", -]; - -pub 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(()) - } -} - -/// Sanitize error message for use with QuickJS/rquickjs. -/// Some messages (e.g. from SQLite "Invalid symbol 45, offset 19") can trigger -/// "Invalid symbol" when rquickjs creates the JS exception — avoid comma and hyphen. -fn sanitize_error_message(msg: &str) -> String { - msg.chars() - .map(|c| { - if c == ',' || c == '-' { - ' ' - } else if c.is_ascii() && !c.is_ascii_control() { - c - } else if c == '\n' || c == '\r' || c == '\t' { - ' ' - } else { - '?' - } - }) - .collect() -} - -pub fn js_err(msg: impl AsRef) -> rquickjs::Error { - let sanitized = sanitize_error_message(msg.as_ref()); - rquickjs::Error::new_from_js_message("skill", "RuntimeError", sanitized) -} diff --git a/src-tauri/src/services/quickjs-libs/service.rs b/src-tauri/src/services/quickjs-libs/service.rs deleted file mode 100644 index f9b886d0b..000000000 --- a/src-tauri/src/services/quickjs-libs/service.rs +++ /dev/null @@ -1,457 +0,0 @@ -//! TdlibV8Service — High-level TDLib service using V8 runtime. -//! -//! Manages TDLib client instances running in V8 with tdweb. -//! Provides async send/receive interface and update broadcasting. - -use std::path::PathBuf; - -use serde::{Deserialize, Serialize}; -use tokio::sync::{broadcast, mpsc, oneshot}; - -use super::storage::IdbStorage; - -/// Configuration for a TDLib client. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -pub struct TdClientConfig { - /// API ID from my.telegram.org - pub api_id: i32, - /// API hash from my.telegram.org - pub api_hash: String, - /// Database directory name - pub database_directory: String, - /// Files directory name - pub files_directory: String, - /// Use test DC - #[serde(default)] - pub use_test_dc: bool, - /// Use file database - #[serde(default = "default_true")] - pub use_file_database: bool, - /// Use chat info database - #[serde(default = "default_true")] - pub use_chat_info_database: bool, - /// Use message database - #[serde(default = "default_true")] - pub use_message_database: bool, - /// System language code - #[serde(default = "default_lang")] - pub system_language_code: String, - /// Device model - #[serde(default = "default_device")] - pub device_model: String, - /// Application version - #[serde(default = "default_version")] - pub application_version: String, -} - -#[allow(dead_code)] -fn default_true() -> bool { - true -} - -#[allow(dead_code)] -fn default_lang() -> String { - "en".to_string() -} - -#[allow(dead_code)] -fn default_device() -> String { - "Desktop".to_string() -} - -#[allow(dead_code)] -fn default_version() -> String { - "1.0.0".to_string() -} - -impl Default for TdClientConfig { - fn default() -> Self { - Self { - api_id: 0, - api_hash: String::new(), - database_directory: "tdlib".to_string(), - files_directory: "tdlib_files".to_string(), - use_test_dc: false, - use_file_database: true, - use_chat_info_database: true, - use_message_database: true, - system_language_code: "en".to_string(), - device_model: "Desktop".to_string(), - application_version: "1.0.0".to_string(), - } - } -} - -/// A TDLib update received from the server. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -pub struct TdUpdate { - /// The update type (e.g., "updateNewMessage") - #[serde(rename = "@type")] - pub update_type: String, - /// The full update data - #[serde(flatten)] - pub data: serde_json::Value, -} - -/// Messages sent to the TDLib service. -#[derive(Debug)] -#[allow(dead_code)] -pub enum TdServiceMessage { - /// Send a TDLib query - Send { - user_id: String, - query: serde_json::Value, - reply: oneshot::Sender>, - }, - /// Get current auth state - GetAuthState { - user_id: String, - reply: oneshot::Sender>, - }, - /// Create a new client - CreateClient { - user_id: String, - config: TdClientConfig, - reply: oneshot::Sender>, - }, - /// Destroy a client - DestroyClient { - user_id: String, - reply: oneshot::Sender>, - }, - /// Stop the service - Stop, -} - -/// State for a single TDLib client. -#[allow(dead_code)] -struct TdClientState { - /// Current authorization state - auth_state: serde_json::Value, - /// Whether the client is ready - is_ready: bool, -} - -/// TDLib V8 Service that manages TDLib clients. -#[allow(dead_code)] -pub struct TdlibV8Service { - /// Data directory for TDLib databases - data_dir: PathBuf, - /// Storage layer for IndexedDB emulation - storage: IdbStorage, - /// Message sender for the service - tx: mpsc::Sender, - /// Update broadcaster - update_tx: broadcast::Sender<(String, TdUpdate)>, -} - -#[allow(dead_code)] -impl TdlibV8Service { - /// Create a new TDLib V8 service. - pub async fn new(data_dir: PathBuf) -> Result { - let storage = IdbStorage::new(&data_dir)?; - let (tx, _rx) = mpsc::channel(64); - let (update_tx, _) = broadcast::channel(256); - - let service = Self { - data_dir, - storage, - tx, - update_tx, - }; - - Ok(service) - } - - /// Get a sender for the service. - pub fn sender(&self) -> mpsc::Sender { - self.tx.clone() - } - - /// Subscribe to TDLib updates. - /// Returns a receiver that will receive (user_id, update) tuples. - pub fn subscribe(&self) -> broadcast::Receiver<(String, TdUpdate)> { - self.update_tx.subscribe() - } - - /// Send a query to TDLib. - pub async fn send( - &self, - user_id: &str, - query: serde_json::Value, - ) -> Result { - let (reply_tx, reply_rx) = oneshot::channel(); - - self.tx - .send(TdServiceMessage::Send { - user_id: user_id.to_string(), - query, - reply: reply_tx, - }) - .await - .map_err(|e| format!("Failed to send query: {e}"))?; - - reply_rx - .await - .map_err(|_| "Service did not respond".to_string())? - } - - /// Get the current authorization state. - pub async fn get_auth_state(&self, user_id: &str) -> Result { - let (reply_tx, reply_rx) = oneshot::channel(); - - self.tx - .send(TdServiceMessage::GetAuthState { - user_id: user_id.to_string(), - reply: reply_tx, - }) - .await - .map_err(|e| format!("Failed to get auth state: {e}"))?; - - reply_rx - .await - .map_err(|_| "Service did not respond".to_string())? - } - - /// Create a new TDLib client for a user. - pub async fn create_client( - &self, - user_id: &str, - config: TdClientConfig, - ) -> Result<(), String> { - let (reply_tx, reply_rx) = oneshot::channel(); - - self.tx - .send(TdServiceMessage::CreateClient { - user_id: user_id.to_string(), - config, - reply: reply_tx, - }) - .await - .map_err(|e| format!("Failed to create client: {e}"))?; - - reply_rx - .await - .map_err(|_| "Service did not respond".to_string())? - } - - /// Destroy a TDLib client. - pub async fn destroy_client(&self, user_id: &str) -> Result<(), String> { - let (reply_tx, reply_rx) = oneshot::channel(); - - self.tx - .send(TdServiceMessage::DestroyClient { - user_id: user_id.to_string(), - reply: reply_tx, - }) - .await - .map_err(|e| format!("Failed to destroy client: {e}"))?; - - reply_rx - .await - .map_err(|_| "Service did not respond".to_string())? - } -} - -/// TDLib client adapter that wraps a V8 runtime. -/// -/// This is a placeholder for the full tdweb integration. -/// In the full implementation, this would: -/// 1. Load the tdweb JavaScript bundle -/// 2. Initialize TdClient with the WASM module -/// 3. Handle send/receive through the V8 runtime -#[allow(dead_code)] -pub struct TdClientAdapter { - user_id: String, - config: TdClientConfig, - data_dir: PathBuf, - storage: IdbStorage, - auth_state: serde_json::Value, - query_id: u64, -} - -#[allow(dead_code)] -impl TdClientAdapter { - /// Create a new TDLib client adapter. - pub fn new( - user_id: String, - config: TdClientConfig, - data_dir: PathBuf, - storage: IdbStorage, - ) -> Self { - Self { - user_id, - config, - data_dir, - storage, - auth_state: serde_json::json!({ - "@type": "authorizationStateWaitTdlibParameters" - }), - query_id: 0, - } - } - - /// Initialize the TDLib client. - /// - /// This would load the tdweb bundle and initialize the WASM module. - pub async fn init(&mut self) -> Result<(), String> { - log::info!("[tdlib:{}] Initializing TDLib client", self.user_id); - - // TODO: Load tdweb.js and libtdjson.wasm - // TODO: Create TdClient instance in V8 - // TODO: Set up update handlers - - // For now, simulate initialization - self.auth_state = serde_json::json!({ - "@type": "authorizationStateWaitTdlibParameters" - }); - - Ok(()) - } - - /// Send a query to TDLib. - pub async fn send(&mut self, query: serde_json::Value) -> Result { - self.query_id += 1; - let query_id = self.query_id; - - log::debug!( - "[tdlib:{}] Sending query {}: {:?}", - self.user_id, - query_id, - query.get("@type") - ); - - // TODO: Execute query through V8/tdweb - // For now, return a placeholder response - - let query_type = query - .get("@type") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - - match query_type { - "getAuthorizationState" => Ok(self.auth_state.clone()), - "setTdlibParameters" => { - self.auth_state = serde_json::json!({ - "@type": "authorizationStateWaitPhoneNumber" - }); - Ok(serde_json::json!({ "@type": "ok" })) - } - _ => Ok(serde_json::json!({ - "@type": "error", - "code": 400, - "message": "TDLib not fully initialized - tdweb integration pending" - })), - } - } - - /// Get the current authorization state. - pub fn get_auth_state(&self) -> serde_json::Value { - self.auth_state.clone() - } - - /// Destroy the client. - pub async fn destroy(&mut self) -> Result<(), String> { - log::info!("[tdlib:{}] Destroying TDLib client", self.user_id); - - // TODO: Properly close TDLib client - // TODO: Cleanup V8 runtime - - Ok(()) - } -} - -/// JavaScript code to initialize the TDLib bridge in V8. -/// -/// This provides the `__tdlib_send` and `__tdlib_get_auth_state` functions -/// that the bootstrap.js exposes to skills. -#[allow(dead_code)] -pub const TDLIB_BRIDGE_JS: &str = r#" -// TDLib Bridge for V8 Runtime -// This will be replaced with actual tdweb integration - -(function() { - // Client state - const clients = new Map(); - let defaultClient = null; - - // Create a mock client for now - class MockTdClient { - constructor(options) { - this.options = options; - this.authState = { '@type': 'authorizationStateWaitTdlibParameters' }; - this.queryId = 0; - this.callbacks = new Map(); - } - - async send(query) { - this.queryId++; - const queryType = query['@type']; - - console.log('[TDLib] Send:', queryType); - - // Handle known query types - switch (queryType) { - case 'getAuthorizationState': - return this.authState; - - case 'setTdlibParameters': - this.authState = { '@type': 'authorizationStateWaitPhoneNumber' }; - return { '@type': 'ok' }; - - case 'setAuthenticationPhoneNumber': - this.authState = { '@type': 'authorizationStateWaitCode' }; - return { '@type': 'ok' }; - - default: - return { - '@type': 'error', - 'code': 400, - 'message': 'TDLib not fully initialized - waiting for tdweb integration' - }; - } - } - - getAuthState() { - return this.authState; - } - } - - // Initialize default client - function initClient(userId, options) { - const client = new MockTdClient(options); - clients.set(userId, client); - if (!defaultClient) { - defaultClient = client; - } - return client; - } - - // Get or create client - function getClient(userId) { - if (!clients.has(userId)) { - initClient(userId, {}); - } - return clients.get(userId); - } - - // Export to global scope - globalThis.__tdlib_send = async function(userId, query) { - const client = getClient(userId); - return await client.send(query); - }; - - globalThis.__tdlib_get_auth_state = function(userId) { - const client = getClient(userId); - return client.getAuthState(); - }; - - globalThis.__tdlib_init = function(userId, options) { - return initClient(userId, options); - }; - - console.log('[TDLib] Bridge initialized (mock mode)'); -})(); -"#; diff --git a/src-tauri/src/services/quickjs-libs/storage.rs b/src-tauri/src/services/quickjs-libs/storage.rs deleted file mode 100644 index 955c9ffb0..000000000 --- a/src-tauri/src/services/quickjs-libs/storage.rs +++ /dev/null @@ -1,673 +0,0 @@ -//! IndexedDB Storage Layer (SQLite-backed) -//! -//! Provides persistent storage for: -//! - IndexedDB API (for tdweb) -//! - Skill databases (db bridge) -//! - Skill key-value store (store bridge) - -use parking_lot::RwLock; -use rusqlite::{params, Connection, OpenFlags}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -/// Result of opening an IndexedDB database. -/// Used by the IndexedDB emulation layer for tdweb. -#[derive(Debug, Clone, serde::Serialize)] -pub struct IdbOpenResult { - /// Whether a version upgrade is needed. - pub needs_upgrade: bool, - /// The previous version (0 if new database). - pub old_version: u32, - /// List of existing object store names. - pub object_stores: Vec, -} - -/// IndexedDB-compatible storage backed by SQLite. -#[derive(Clone)] -pub struct IdbStorage { - /// Base directory for all databases. - data_dir: PathBuf, - /// Open database connections (db_name -> connection). - connections: Arc>>>>, - /// Database version info (used by IndexedDB emulation). - #[allow(dead_code)] - versions: Arc>>, -} - -impl IdbStorage { - /// Create a new IdbStorage instance. - pub fn new(data_dir: &Path) -> Result { - std::fs::create_dir_all(data_dir) - .map_err(|e| format!("Failed to create data directory: {e}"))?; - - Ok(Self { - data_dir: data_dir.to_path_buf(), - connections: Arc::new(RwLock::new(HashMap::new())), - versions: Arc::new(RwLock::new(HashMap::new())), - }) - } - - /// Get or create a database connection. - fn get_connection(&self, db_name: &str) -> Result>, String> { - // Check if already open - if let Some(conn) = self.connections.read().get(db_name) { - return Ok(conn.clone()); - } - - // Open/create the database - let db_path = self.data_dir.join(format!("{}.sqlite", db_name)); - let conn = Connection::open_with_flags( - &db_path, - OpenFlags::SQLITE_OPEN_READ_WRITE - | OpenFlags::SQLITE_OPEN_CREATE - | OpenFlags::SQLITE_OPEN_NO_MUTEX, - ) - .map_err(|e| format!("Failed to open database '{}': {}", db_name, e))?; - - // Initialize schema - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS _idb_meta ( - key TEXT PRIMARY KEY, - value TEXT - ); - CREATE TABLE IF NOT EXISTS _idb_stores ( - name TEXT PRIMARY KEY, - key_path TEXT, - auto_increment INTEGER DEFAULT 0 - ); - "#, - ) - .map_err(|e| format!("Failed to initialize database schema: {e}"))?; - - let conn = Arc::new(parking_lot::Mutex::new(conn)); - self.connections - .write() - .insert(db_name.to_string(), conn.clone()); - - Ok(conn) - } - - /// Open or create an IndexedDB database. - pub fn open_database(&self, name: &str, version: u32) -> Result { - let conn = self.get_connection(name)?; - let conn_guard = conn.lock(); - - // Get current version - let current_version: u32 = conn_guard - .query_row( - "SELECT value FROM _idb_meta WHERE key = 'version'", - [], - |row| row.get::<_, String>(0), - ) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - - // Check if upgrade needed - let needs_upgrade = version > current_version; - - if needs_upgrade { - // Update version - conn_guard - .execute( - "INSERT OR REPLACE INTO _idb_meta (key, value) VALUES ('version', ?)", - params![version.to_string()], - ) - .map_err(|e| format!("Failed to update version: {e}"))?; - } - - // Get object store names - let mut stmt = conn_guard - .prepare("SELECT name FROM _idb_stores") - .map_err(|e| format!("Failed to query object stores: {e}"))?; - - let object_stores: Vec = stmt - .query_map([], |row| row.get(0)) - .map_err(|e| format!("Failed to fetch object stores: {e}"))? - .filter_map(|r| r.ok()) - .collect(); - - self.versions.write().insert(name.to_string(), version); - - Ok(IdbOpenResult { - needs_upgrade, - old_version: current_version, - object_stores, - }) - } - - /// Close a database connection. - pub fn close_database(&self, name: &str) { - self.connections.write().remove(name); - self.versions.write().remove(name); - } - - /// Delete a database. - 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() { - std::fs::remove_file(&db_path) - .map_err(|e| format!("Failed to delete database: {e}"))?; - } - Ok(()) - } - - /// Create an object store. - pub fn create_object_store( - &self, - db_name: &str, - store_name: &str, - key_path: Option<&str>, - auto_increment: bool, - ) -> Result<(), String> { - let conn = self.get_connection(db_name)?; - let conn_guard = conn.lock(); - - // Register the store - conn_guard - .execute( - "INSERT OR REPLACE INTO _idb_stores (name, key_path, auto_increment) VALUES (?, ?, ?)", - params![store_name, key_path, auto_increment as i32], - ) - .map_err(|e| format!("Failed to create object store: {e}"))?; - - // Create the data table - let table_name = format!("store_{}", sanitize_name(store_name)); - conn_guard - .execute( - &format!( - r#" - CREATE TABLE IF NOT EXISTS "{}" ( - key TEXT PRIMARY KEY, - value TEXT - ) - "#, - table_name - ), - [], - ) - .map_err(|e| format!("Failed to create store table: {e}"))?; - - Ok(()) - } - - /// 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.lock(); - - // Remove from registry - conn_guard - .execute("DELETE FROM _idb_stores WHERE name = ?", params![store_name]) - .map_err(|e| format!("Failed to delete object store: {e}"))?; - - // Drop the table - let table_name = format!("store_{}", sanitize_name(store_name)); - conn_guard - .execute(&format!("DROP TABLE IF EXISTS \"{}\"", table_name), []) - .map_err(|e| format!("Failed to drop store table: {e}"))?; - - Ok(()) - } - - /// Get a value from an object store. - pub fn get( - &self, - db_name: &str, - store_name: &str, - key: &serde_json::Value, - ) -> Result, String> { - let conn = self.get_connection(db_name)?; - 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()); - - let result: Option = conn_guard - .query_row( - &format!("SELECT value FROM \"{}\" WHERE key = ?", table_name), - params![key_str], - |row| row.get(0), - ) - .ok(); - - match result { - Some(value_str) => { - let value: serde_json::Value = - serde_json::from_str(&value_str).unwrap_or(serde_json::Value::Null); - Ok(Some(value)) - } - None => Ok(None), - } - } - - /// Put a value into an object store. - pub fn put( - &self, - db_name: &str, - store_name: &str, - key: &serde_json::Value, - value: &serde_json::Value, - ) -> Result<(), String> { - let conn = self.get_connection(db_name)?; - 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()); - let value_str = serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()); - - conn_guard - .execute( - &format!( - "INSERT OR REPLACE INTO \"{}\" (key, value) VALUES (?, ?)", - table_name - ), - params![key_str, value_str], - ) - .map_err(|e| format!("Failed to put value: {e}"))?; - - Ok(()) - } - - /// Delete a value from an object store. - 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.lock(); - - let table_name = format!("store_{}", sanitize_name(store_name)); - let key_str = serde_json::to_string(key).unwrap_or_else(|_| "null".to_string()); - - conn_guard - .execute( - &format!("DELETE FROM \"{}\" WHERE key = ?", table_name), - params![key_str], - ) - .map_err(|e| format!("Failed to delete value: {e}"))?; - - Ok(()) - } - - /// Clear all values from an object store. - pub fn clear(&self, db_name: &str, store_name: &str) -> Result<(), String> { - let conn = self.get_connection(db_name)?; - let conn_guard = conn.lock(); - - let table_name = format!("store_{}", sanitize_name(store_name)); - - conn_guard - .execute(&format!("DELETE FROM \"{}\"", table_name), []) - .map_err(|e| format!("Failed to clear store: {e}"))?; - - Ok(()) - } - - /// Get all values from an object store. - pub fn get_all( - &self, - db_name: &str, - store_name: &str, - count: Option, - ) -> Result, String> { - let conn = self.get_connection(db_name)?; - 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(); - - let mut stmt = conn_guard - .prepare(&format!("SELECT value FROM \"{}\"{}", table_name, limit)) - .map_err(|e| format!("Failed to query values: {e}"))?; - - let values: Vec = stmt - .query_map([], |row| row.get::<_, String>(0)) - .map_err(|e| format!("Failed to fetch values: {e}"))? - .filter_map(|r| r.ok()) - .filter_map(|s| serde_json::from_str(&s).ok()) - .collect(); - - Ok(values) - } - - /// Get all keys from an object store. - pub fn get_all_keys( - &self, - db_name: &str, - store_name: &str, - count: Option, - ) -> Result, String> { - let conn = self.get_connection(db_name)?; - 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(); - - let mut stmt = conn_guard - .prepare(&format!("SELECT key FROM \"{}\"{}", table_name, limit)) - .map_err(|e| format!("Failed to query keys: {e}"))?; - - let keys: Vec = stmt - .query_map([], |row| row.get::<_, String>(0)) - .map_err(|e| format!("Failed to fetch keys: {e}"))? - .filter_map(|r| r.ok()) - .filter_map(|s| serde_json::from_str(&s).ok()) - .collect(); - - Ok(keys) - } - - /// Count values in an object store. - pub fn count(&self, db_name: &str, store_name: &str) -> Result { - let conn = self.get_connection(db_name)?; - let conn_guard = conn.lock(); - - let table_name = format!("store_{}", sanitize_name(store_name)); - - let count: u32 = conn_guard - .query_row( - &format!("SELECT COUNT(*) FROM \"{}\"", table_name), - [], - |row| row.get(0), - ) - .map_err(|e| format!("Failed to count values: {e}"))?; - - Ok(count) - } - - // ======================================================================== - // Skill Database Bridge Methods - // ======================================================================== - - /// Execute SQL and return affected row count. - pub fn skill_db_exec( - &self, - skill_id: &str, - sql: &str, - params: &[serde_json::Value], - ) -> Result { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - let params: Vec> = params - .iter() - .map(|v| -> Box { Box::new(json_to_sql(v)) }) - .collect(); - - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); - - conn_guard - .execute(sql, params_refs.as_slice()) - .map_err(|e| format!("SQL exec failed: {e}")) - } - - /// Execute SQL and return a single row. - pub fn skill_db_get( - &self, - skill_id: &str, - sql: &str, - params: &[serde_json::Value], - ) -> Result { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - let params: Vec> = params - .iter() - .map(|v| -> Box { Box::new(json_to_sql(v)) }) - .collect(); - - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); - - let mut stmt = conn_guard - .prepare(sql) - .map_err(|e| format!("SQL prepare failed: {e}"))?; - - let column_count = stmt.column_count(); - // Capture column names before the closure to avoid borrow checker issues - let column_names: Vec = (0..column_count) - .map(|i| stmt.column_name(i).unwrap_or("?").to_string()) - .collect(); - - let result = stmt.query_row(params_refs.as_slice(), |row| { - let mut obj = serde_json::Map::new(); - for (i, col_name) in column_names.iter().enumerate() { - let value = sql_to_json(row, i); - obj.insert(col_name.clone(), value); - } - Ok(serde_json::Value::Object(obj)) - }); - - match result { - Ok(v) => Ok(v), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(serde_json::Value::Null), - Err(e) => Err(format!("SQL query failed: {e}")), - } - } - - /// Execute SQL and return all rows. - pub fn skill_db_all( - &self, - skill_id: &str, - sql: &str, - params: &[serde_json::Value], - ) -> Result { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - let params: Vec> = params - .iter() - .map(|v| -> Box { Box::new(json_to_sql(v)) }) - .collect(); - - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); - - let mut stmt = conn_guard - .prepare(sql) - .map_err(|e| format!("SQL prepare failed: {e}"))?; - - let column_count = stmt.column_count(); - let column_names: Vec = (0..column_count) - .map(|i| stmt.column_name(i).unwrap_or("?").to_string()) - .collect(); - - let rows = stmt - .query_map(params_refs.as_slice(), |row| { - let mut obj = serde_json::Map::new(); - for (i, name) in column_names.iter().enumerate() { - let value = sql_to_json(row, i); - obj.insert(name.clone(), value); - } - Ok(serde_json::Value::Object(obj)) - }) - .map_err(|e| format!("SQL query failed: {e}"))? - .filter_map(|r| r.ok()) - .collect::>(); - - Ok(serde_json::Value::Array(rows)) - } - - /// Get a key-value pair. - pub fn skill_kv_get(&self, skill_id: &str, key: &str) -> Result { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - // Ensure KV table exists - conn_guard - .execute( - "CREATE TABLE IF NOT EXISTS _kv (key TEXT PRIMARY KEY, value TEXT)", - [], - ) - .map_err(|e| format!("Failed to create KV table: {e}"))?; - - let result: Option = conn_guard - .query_row("SELECT value FROM _kv WHERE key = ?", params![key], |row| { - row.get(0) - }) - .ok(); - - match result { - Some(v) => { - serde_json::from_str(&v).map_err(|e| format!("Failed to parse value: {e}")) - } - None => Ok(serde_json::Value::Null), - } - } - - /// Set a key-value pair. - pub fn skill_kv_set( - &self, - skill_id: &str, - key: &str, - value: &serde_json::Value, - ) -> Result<(), String> { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - // Ensure KV table exists - conn_guard - .execute( - "CREATE TABLE IF NOT EXISTS _kv (key TEXT PRIMARY KEY, value TEXT)", - [], - ) - .map_err(|e| format!("Failed to create KV table: {e}"))?; - - let value_str = serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()); - - conn_guard - .execute( - "INSERT OR REPLACE INTO _kv (key, value) VALUES (?, ?)", - params![key, value_str], - ) - .map_err(|e| format!("Failed to set value: {e}"))?; - - Ok(()) - } - - // ======================================================================== - // Skill Store Bridge Methods (same as KV but different namespace) - // ======================================================================== - - /// Get a store value. - pub fn skill_store_get(&self, skill_id: &str, key: &str) -> Result { - self.skill_kv_get(skill_id, &format!("_store_{}", key)) - } - - /// Set a store value. - pub fn skill_store_set( - &self, - skill_id: &str, - key: &str, - value: &serde_json::Value, - ) -> Result<(), String> { - self.skill_kv_set(skill_id, &format!("_store_{}", key), value) - } - - /// Delete a store value. - 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.lock(); - - conn_guard - .execute( - "DELETE FROM _kv WHERE key = ?", - params![format!("_store_{}", key)], - ) - .map_err(|e| format!("Failed to delete value: {e}"))?; - - Ok(()) - } - - /// List all store keys. - pub fn skill_store_keys(&self, skill_id: &str) -> Result, String> { - let db_name = format!("skill_{}", skill_id); - let conn = self.get_connection(&db_name)?; - let conn_guard = conn.lock(); - - // Ensure KV table exists - conn_guard - .execute( - "CREATE TABLE IF NOT EXISTS _kv (key TEXT PRIMARY KEY, value TEXT)", - [], - ) - .map_err(|e| format!("Failed to create KV table: {e}"))?; - - let mut stmt = conn_guard - .prepare("SELECT key FROM _kv WHERE key LIKE '_store_%'") - .map_err(|e| format!("Failed to query keys: {e}"))?; - - let keys: Vec = stmt - .query_map([], |row| row.get::<_, String>(0)) - .map_err(|e| format!("Failed to fetch keys: {e}"))? - .filter_map(|r| r.ok()) - .map(|k| k.strip_prefix("_store_").unwrap_or(&k).to_string()) - .collect(); - - Ok(keys) - } -} - -/// Sanitize a name for use as a table name. -fn sanitize_name(name: &str) -> String { - name.chars() - .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) - .collect() -} - -/// Convert a JSON value to a SQLite value. -fn json_to_sql(v: &serde_json::Value) -> rusqlite::types::Value { - match v { - serde_json::Value::Null => rusqlite::types::Value::Null, - serde_json::Value::Bool(b) => rusqlite::types::Value::Integer(*b as i64), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - rusqlite::types::Value::Integer(i) - } else if let Some(f) = n.as_f64() { - rusqlite::types::Value::Real(f) - } else { - rusqlite::types::Value::Text(n.to_string()) - } - } - serde_json::Value::String(s) => rusqlite::types::Value::Text(s.clone()), - serde_json::Value::Array(_) | serde_json::Value::Object(_) => { - rusqlite::types::Value::Text(v.to_string()) - } - } -} - -/// Convert a SQLite row value to JSON. -fn sql_to_json(row: &rusqlite::Row, idx: usize) -> serde_json::Value { - use rusqlite::types::ValueRef; - - match row.get_ref(idx) { - Ok(ValueRef::Null) => serde_json::Value::Null, - Ok(ValueRef::Integer(i)) => serde_json::json!(i), - Ok(ValueRef::Real(f)) => serde_json::json!(f), - Ok(ValueRef::Text(s)) => { - let s = String::from_utf8_lossy(s).to_string(); - // Try to parse as JSON - serde_json::from_str(&s).unwrap_or_else(|_| serde_json::json!(s)) - } - Ok(ValueRef::Blob(b)) => { - serde_json::json!(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - b - )) - } - Err(_) => serde_json::Value::Null, - } -}