diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml index 2a28a1303..d85755857 100644 --- a/.github/workflows/package-and-publish.yml +++ b/.github/workflows/package-and-publish.yml @@ -402,127 +402,6 @@ jobs: name: ${{ steps.file-info.outputs.filename }} path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }} - # package-android: - # name: Build and package Android APK - # needs: [get-version, check-version, create-release] - # if: ${{ !failure() && !cancelled() && needs.check-version.outputs.should-skip != 'true' }} - # environment: ${{ github.ref == 'refs/heads/main' && 'Production' || '' }} - # runs-on: ubuntu-22.04 - # permissions: - # contents: write - # steps: - # - name: Checkout - # uses: actions/checkout@v4 - # with: - # fetch-depth: 1 - # ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - - # - name: Setup Node.js 24.x - # uses: actions/setup-node@v4 - # with: - # node-version: 24.x - # cache: 'yarn' - - # - name: Install Rust stable - # uses: dtolnay/rust-toolchain@stable - # with: - # targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android - - # - name: Setup Java 17 - # uses: actions/setup-java@v4 - # with: - # distribution: 'temurin' - # java-version: '17' - - # - name: Setup Android SDK - # uses: android-actions/setup-android@v3 - - # - name: Install Android NDK - # run: sdkmanager --install "ndk;27.0.12077973" - - # - name: Set NDK path - # run: echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV - - # - name: Cache node modules - # id: yarn-cache - # uses: actions/cache@v4 - # with: - # path: node_modules - # key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }} - # restore-keys: | - # ${{ runner.os }}-build- - - # - name: Install dependencies - # if: steps.yarn-cache.outputs.cache-hit != 'true' - # run: yarn install --frozen-lockfile - - # - name: Build frontend - # run: yarn build - # env: - # NODE_ENV: production - # VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }} - # VITE_TELEGRAM_BOT_USERNAME: ${{ secrets.VITE_TELEGRAM_BOT_USERNAME }} - # VITE_TELEGRAM_BOT_ID: ${{ secrets.VITE_TELEGRAM_BOT_ID }} - # VITE_TELEGRAM_API_ID: ${{ secrets.VITE_TELEGRAM_API_ID }} - # VITE_TELEGRAM_API_HASH: ${{ secrets.VITE_TELEGRAM_API_HASH }} - # VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} - # VITE_SKILLS_GITHUB_REPO: ${{ vars.VITE_SKILLS_GITHUB_REPO }} - # VITE_DEBUG: ${{ vars.VITE_DEBUG }} - - # - name: Initialize Tauri Android - # run: npx tauri android init - - # - name: Decode keystore - # if: needs.get-version.outputs.should-publish != '' - # env: - # ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} - # run: | - # echo "$ANDROID_KEYSTORE" | base64 --decode > ${{ github.workspace }}/release.jks - - # - name: Build Android APK (signed) - # if: needs.get-version.outputs.should-publish != '' - # env: - # BASE_URL: ${{ vars.BASE_URL }} - # ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/release.jks - # ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - # ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - # ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - # run: npx tauri android build --apk - - # - name: Build Android APK (unsigned) - # if: needs.get-version.outputs.should-publish == '' - # env: - # BASE_URL: ${{ vars.BASE_URL }} - # run: npx tauri android build --apk - - # - name: Find APK - # id: find-apk - # run: | - # APK_PATH=$(find gen/android -name '*.apk' -type f | head -1) - # if [ -z "$APK_PATH" ]; then - # echo "No APK found" - # exit 1 - # fi - # APK_FILENAME=$(basename "$APK_PATH") - # echo "path=$APK_PATH" >> $GITHUB_OUTPUT - # echo "filename=$APK_FILENAME" >> $GITHUB_OUTPUT - - # - name: Upload APK artifact - # uses: actions/upload-artifact@v4 - # with: - # name: android-apk - # path: ${{ steps.find-apk.outputs.path }} - - # - name: Upload APK to release - # if: needs.get-version.outputs.should-publish != '' - # env: - # RELEASE_ID: ${{ needs.create-release.outputs.releaseId }} - # run: | - # curl -X POST -H "Authorization: Bearer $XGH_TOKEN" \ - # -H "Content-Type: application/octet-stream" \ - # --data-binary "@${{ steps.find-apk.outputs.path }}" \ - # "https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=${{ steps.find-apk.outputs.filename }}" - publish-release: runs-on: ubuntu-latest # needs: [get-version, check-version, create-release, package-tauri, package-android] diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 9287b0405..3f3fcd929 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -75,11 +75,15 @@ jobs: require('fs').writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n'); " - # Update Cargo.toml to match - sed -i 's/^version = .*/version = "'"$NEW_VERSION"'"/' src-tauri/Cargo.toml + # Update tauri.conf.json to match + node -e " + const c = require('./src-tauri/tauri.conf.json'); + c.version = process.env.NEW_VERSION; + require('fs').writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(c, null, 2) + '\n'); + " - # Single commit and tag for both npm and Cargo - git add package.json src-tauri/Cargo.toml + # Single commit and tag for package.json and tauri.conf.json + git add package.json src-tauri/tauri.conf.json git commit -m "chore: bump version to $NEW_VERSION [skip ci]" git tag "v$NEW_VERSION" diff --git a/.gitignore b/.gitignore index 8e64f0c04..bb66f93d6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,10 @@ dist-ssr .env.local .env.*.local +# CI secrets for local testing (contains real tokens) +scripts/ci-secrets.json +scripts/ci-secrets.local.json + # Editor directories and files .idea .DS_Store diff --git a/package.json b/package.json index 693be6cb9..d51292107 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,19 @@ "scripts": { "dev": "vite", "dev:web": "vite", + "dev:app": "source scripts/load-dotenv.sh && tauri dev", "build": "tsc && vite build", "build:app": "yarn skills:build && tsc && vite build", "compile": "tsc --noEmit", "preview": "vite preview", "tauri": "tauri", - "macos:build": "tauri build --debug --bundles app && ./src-tauri/scripts/bundle-tdlib-macos.sh debug", - "macos:build:release": "tauri build --bundles app dmg && ./src-tauri/scripts/bundle-tdlib-macos.sh release", + "macos:build:debug": "yarn macos:build:release --debug", + "macos:build:release": "source scripts/load-dotenv.sh && tauri build --bundles app dmg", + "macos:build:sign:release": "source scripts/load-env.sh && tauri build --bundles app dmg", "macos:run": "open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'", - "macos:dev": "tauri build --debug --bundles app && ./src-tauri/scripts/bundle-tdlib-macos.sh debug && open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'", + "macos:dev": "yarn macos:build:debug && open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'", "android:dev": "tauri android dev", "android:build": "tauri android build", - "dev:app": "RUST_BACKTRACE=1 RUST_LOG=info tauri dev", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/scripts/ci-event.json b/scripts/ci-event.json new file mode 100644 index 000000000..b5c4baa3d --- /dev/null +++ b/scripts/ci-event.json @@ -0,0 +1,16 @@ +{ + "ref": "refs/heads/develop", + "before": "0000000000000000000000000000000000000000", + "after": "19281e16457e7c8ff8e6bda6ceda77f0880d10d2", + "repository": { + "full_name": "vezuresdotxyz/alphahuman-frontend-runner", + "default_branch": "main", + "name": "alphahuman-frontend-runner", + "owner": { "login": "vezuresdotxyz" } + }, + "head_commit": { + "id": "19281e16457e7c8ff8e6bda6ceda77f0880d10d2", + "message": "local test build" + }, + "sender": { "login": "local-dev" } +} diff --git a/scripts/ci-secrets.example.json b/scripts/ci-secrets.example.json new file mode 100644 index 000000000..e17fb8a53 --- /dev/null +++ b/scripts/ci-secrets.example.json @@ -0,0 +1,28 @@ +{ + "secrets": { + "XGH_TOKEN": "", + "GITHUB_TOKEN": "", + "UPDATER_GIST_URL": "", + "UPDATER_GIST_ID": "", + "UPDATER_PUBLIC_KEY": "", + "UPDATER_PRIVATE_KEY": "", + "UPDATER_PRIVATE_KEY_PASSWORD": "", + "APPLE_CERTIFICATE_BASE64": "", + "APPLE_CERTIFICATE_PASSWORD": "", + "APPLE_SIGNING_IDENTITY": "", + "APPLE_ID": "", + "APPLE_APP_SPECIFIC_PASSWORD": "", + "APPLE_TEAM_ID": "", + "VITE_TELEGRAM_BOT_USERNAME": "", + "VITE_TELEGRAM_BOT_ID": "", + "VITE_TELEGRAM_API_ID": "", + "VITE_TELEGRAM_API_HASH": "", + "VITE_SENTRY_DSN": "" + }, + "vars": { + "BASE_URL": "https://localhost", + "VITE_BACKEND_URL": "https://localhost:5005", + "VITE_SKILLS_GITHUB_REPO": "", + "VITE_DEBUG": "true" + } +} diff --git a/scripts/load-dotenv.sh b/scripts/load-dotenv.sh new file mode 100755 index 000000000..d0649cb46 --- /dev/null +++ b/scripts/load-dotenv.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Load .env file into environment variables. +# Usage: +# source scripts/load-dotenv.sh [path/to/.env] +# eval "$(scripts/load-dotenv.sh [path/to/.env])" +# Default path: .env (project root when run from repo root) + +set -e +FILE="${1:-.env}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +RESOLVED="${1:+$1}" +RESOLVED="${RESOLVED:-$ROOT_DIR/.env}" + +if [[ ! -f "$RESOLVED" ]]; then + echo "File not found: $RESOLVED" >&2 + exit 1 +fi + +exports=() +while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" ]] && continue + if [[ "$line" == export\ * ]]; then + line="${line#export }" + fi + if [[ "$line" == *"="* ]]; then + key="${line%%=*}" + key="${key%"${key##*[![:space:]]}"}" + value="${line#*=}" + value="${value#\"}" + value="${value%\"}" + value="${value#\'}" + value="${value%\'}" + [[ -n "$key" ]] && exports+=("$(printf 'export %s=%q' "$key" "$value")") + fi +done < "$RESOLVED" + +joined=$(printf '%s\n' "${exports[@]}") + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "$joined" +else + eval "$joined" +fi diff --git a/scripts/load-env-json.sh b/scripts/load-env-json.sh new file mode 100755 index 000000000..47913701d --- /dev/null +++ b/scripts/load-env-json.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Load key-value JSON into environment variables. +# Usage: +# source scripts/load-env-json.sh path/to/file.json +# eval "$(scripts/load-env-json.sh path/to/file.json)" +# Optional jq filter to select object (default: .): +# source scripts/load-env-json.sh ci-secrets.json '.secrets + .vars' + +set -e +FILE="${1:?Usage: $0 [jq-filter]}" +FILTER="${2:-.}" + +if [[ ! -f "$FILE" ]]; then + echo "File not found: $FILE" >&2 + exit 1 +fi + +if ! command -v jq &>/dev/null; then + echo "jq is required" >&2 + exit 1 +fi + +exports=$(jq -r "${FILTER} | to_entries | .[] | \"export \(.key)=\(.value | @sh)\"" "$FILE") + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "$exports" +else + eval "$exports" +fi diff --git a/scripts/load-env.sh b/scripts/load-env.sh new file mode 100755 index 000000000..0274305a8 --- /dev/null +++ b/scripts/load-env.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Load .env file into environment variables and optional ci-secrets for signing/notarization. +# Usage: +# source scripts/load-env.sh +# eval "$(scripts/load-env.sh)" + +set -e + +source scripts/load-dotenv.sh + +if [[ -f scripts/ci-secrets.local.json ]]; then + source scripts/load-env-json.sh scripts/ci-secrets.local.json + # Tauri notarization expects APPLE_PASSWORD; secrets file uses APPLE_APP_SPECIFIC_PASSWORD + if [[ -z "${APPLE_PASSWORD:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]]; then + export APPLE_PASSWORD="$APPLE_APP_SPECIFIC_PASSWORD" + fi +fi diff --git a/scripts/test-ci-local.sh b/scripts/test-ci-local.sh new file mode 100755 index 000000000..fc05942e1 --- /dev/null +++ b/scripts/test-ci-local.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Test the package-and-publish workflow locally using `act`. +# +# Prerequisites: +# brew install act jq +# +# Setup: +# cp scripts/ci-secrets.example.json scripts/ci-secrets.json +# # Edit scripts/ci-secrets.json with your real values +# +# Usage: +# ./scripts/test-ci-local.sh # Run full workflow via act +# ./scripts/test-ci-local.sh --manual # Run build steps natively on macOS (recommended) +# ./scripts/test-ci-local.sh --list # List available jobs +# ./scripts/test-ci-local.sh --dryrun # Dry-run (show what would execute) + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +# ── Configuration ───────────────────────────────────────────────────────────── + +WORKFLOW=".github/workflows/package-and-publish.yml" +SECRETS_JSON="scripts/ci-secrets.json" +EVENT_JSON="scripts/ci-event.json" + +if [[ ! -f "$SECRETS_JSON" ]]; then + echo "ERROR: $SECRETS_JSON not found." + echo "" + echo "Create it from the example:" + echo " cp scripts/ci-secrets.example.json scripts/ci-secrets.json" + echo " # then fill in your values" + exit 1 +fi + +# ── Generate event payload with current HEAD ────────────────────────────────── + +CURRENT_REF=$(git rev-parse HEAD) +cat > "$EVENT_JSON" < "$SECRETS_FILE" + +# Extract "vars" object → KEY=VALUE +jq -r '.vars // {} | to_entries[] | "\(.key)=\(.value)"' "$SECRETS_JSON" > "$VARS_FILE" + +echo "Loaded $(wc -l < "$SECRETS_FILE" | tr -d ' ') secrets and $(wc -l < "$VARS_FILE" | tr -d ' ') vars from $SECRETS_JSON" + +# ── Common act arguments ────────────────────────────────────────────────────── + +ACT_ARGS=( + -W "$WORKFLOW" + --secret-file "$SECRETS_FILE" + --var-file "$VARS_FILE" + --eventpath "$EVENT_JSON" + -P ubuntu-latest=catthehacker/ubuntu:act-latest + -P macos-latest=-self-hosted +) + +# ── Handle CLI flags ────────────────────────────────────────────────────────── + +if [[ "${1:-}" == "--list" ]]; then + echo "Available jobs in $WORKFLOW:" + act -W "$WORKFLOW" --list + exit 0 +fi + +if [[ "${1:-}" == "--dryrun" ]]; then + echo "Dry-run of workflow:" + act push "${ACT_ARGS[@]}" -n + exit 0 +fi + +# ── Manual macOS-native build (recommended) ─────────────────────────────────── + +if [[ "${1:-}" == "--manual" ]]; then + echo "=== Running build steps manually on macOS host ===" + echo "" + + # Export VITE_* vars from the JSON so the frontend build picks them up + eval "$(jq -r ' + (.secrets // {}) + (.vars // {}) + | to_entries[] + | select(.key | startswith("VITE_")) + | "export \(.key)=\(.value | @sh)" + ' "$SECRETS_JSON")" + + # Step 1: Ensure OpenSSL is installed + echo ">>> Step 1: Ensure OpenSSL is installed" + brew install openssl@3 2>/dev/null || true + + # Step 2: Install Node dependencies + echo ">>> Step 2: Install Node dependencies" + yarn install --frozen-lockfile + + # Step 3: Install skills dependencies and build + echo ">>> Step 3: Build skills" + (cd skills && yarn install --frozen-lockfile && yarn build) + + # Step 4: Build frontend + echo ">>> Step 4: Build frontend" + NODE_ENV=production yarn build + + # Step 5: Build Tauri (aarch64) + echo ">>> Step 5: Build Tauri app (aarch64-apple-darwin)" + yarn tauri build --target aarch64-apple-darwin + + echo "" + echo "=== Build complete ===" + echo "Check src-tauri/target/aarch64-apple-darwin/release/bundle/ for output" + exit 0 +fi + +# ── Default: run full workflow with act ──────────────────────────────────────── +# +# We run the full workflow (not -j single-job) so act executes the dependency +# chain: get-version → check-version → create-release (skipped) → package-tauri. +# Using -j package-tauri alone fails because act can't resolve outputs from +# skipped `needs` jobs. + +echo "=== Testing package-and-publish workflow locally ===" +echo "" +echo "Workflow: $WORKFLOW" +echo "Event: push to develop (from $EVENT_JSON)" +echo "" +echo "NOTE: act uses Docker containers — macOS-specific steps won't work." +echo "For a native macOS build, use: $0 --manual" +echo "" + +act push "${ACT_ARGS[@]}" --verbose diff --git a/skills b/skills index d9dfb8d97..f78b3319d 160000 --- a/skills +++ b/skills @@ -1 +1 @@ -Subproject commit d9dfb8d974609e8b0324d78888fcbd1f711178a7 +Subproject commit f78b3319d803213dee74bd13ec45ef86c1fa34dc diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4960a8fec..9dd5203bf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -12,7 +12,6 @@ dependencies = [ "base64 0.22.1", "chrono", "cron", - "deno_core", "dirs 5.0.1", "dotenvy", "encoding_rs", @@ -26,6 +25,7 @@ dependencies = [ "parking_lot", "rand 0.8.5", "reqwest", + "rquickjs", "rusqlite", "rust_socketio", "serde", @@ -44,12 +44,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.1" @@ -133,6 +127,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -287,7 +287,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix", "slab", "windows-sys 0.61.2", ] @@ -318,7 +318,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix", ] [[package]] @@ -344,7 +344,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -458,53 +458,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" -dependencies = [ - "simd-abstraction", -] - [[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.114", - "which 4.4.2", -] - [[package]] name = "bindgen" version = "0.72.1" @@ -520,26 +479,11 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.114", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -555,18 +499,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake2" version = "0.10.6" @@ -725,7 +657,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver 1.0.27", + "semver", "serde", "serde_json", "thiserror 2.0.18", @@ -903,10 +835,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] -name = "cooked-waker" -version = "5.0.0" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "cookie" @@ -1131,86 +1066,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "serde", - "uuid", -] - [[package]] name = "deflate64" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "deno_core" -version = "0.314.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83138917579676069b423c3eb9be3c1e579f60dc022d85f6ded4c792456255ff" -dependencies = [ - "anyhow", - "bincode", - "bit-set", - "bit-vec", - "bytes", - "cooked-waker", - "deno_core_icudata", - "deno_ops", - "deno_unsync", - "futures", - "libc", - "memoffset", - "parking_lot", - "percent-encoding", - "pin-project", - "serde", - "serde_json", - "serde_v8", - "smallvec", - "sourcemap", - "static_assertions", - "tokio", - "url", - "v8", -] - -[[package]] -name = "deno_core_icudata" -version = "0.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13951ea98c0a4c372f162d669193b4c9d991512de9f2381dd161027f34b26b1" - -[[package]] -name = "deno_ops" -version = "0.190.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f46d4e4f52f26c882b74a9b58810ea75252b807cf0966166ec333077cdfd85" -dependencies = [ - "proc-macro-rules", - "proc-macro2", - "quote", - "strum", - "strum_macros", - "syn 2.0.114", - "thiserror 1.0.69", -] - -[[package]] -name = "deno_unsync" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6742a724e8becb372a74c650a1aefb8924a5b8107f7d75b3848763ea24b27a87" -dependencies = [ - "futures-util", - "parking_lot", - "tokio", -] - [[package]] name = "deranged" version = "0.5.5" @@ -1238,10 +1099,10 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "syn 2.0.114", ] @@ -1433,7 +1294,7 @@ checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" dependencies = [ "cc", "memchr", - "rustc_version 0.4.1", + "rustc_version", "toml 0.9.11+spec-1.1.0", "vswhom", "winreg 0.55.0", @@ -1586,7 +1447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", - "rustc_version 0.4.1", + "rustc_version", ] [[package]] @@ -1611,7 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -1620,6 +1481,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1671,22 +1538,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fslock" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futf" version = "0.1.5" @@ -1923,7 +1774,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix", "windows-link 0.2.1", ] @@ -2123,15 +1974,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "gzip-header" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" -dependencies = [ - "crc32fast", -] - [[package]] name = "h2" version = "0.4.13" @@ -2171,6 +2013,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -2214,15 +2061,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "html5ever" version = "0.29.1" @@ -2497,12 +2335,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" - [[package]] name = "indexmap" version = "1.9.3" @@ -2759,12 +2591,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2836,12 +2662,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2874,7 +2694,7 @@ version = "0.1.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a180dfa6d6f9d1df1e031bcdf0464bbad4f9b326395bfd28f2fa539d8cbc9c2b" dependencies = [ - "bindgen 0.72.1", + "bindgen", "cc", "cmake", "find_cuda_helper", @@ -3000,15 +2820,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3146,32 +2957,12 @@ dependencies = [ "zbus", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.8.5", -] - [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3560,12 +3351,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "outref" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" - [[package]] name = "pango" version = "0.18.3" @@ -3631,12 +3416,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3793,26 +3572,6 @@ dependencies = [ "siphasher 1.0.1", ] -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3865,7 +3624,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -3878,7 +3637,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix", "windows-sys 0.61.2", ] @@ -4008,29 +3767,6 @@ version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" -[[package]] -name = "proc-macro-rules" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" -dependencies = [ - "proc-macro-rules-macros", - "proc-macro2", - "syn 2.0.114", -] - -[[package]] -name = "proc-macro-rules-macros" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207fffb0fe655d1d47f6af98cc2793405e85929bdbc420d685554ff07be27ac7" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -4069,7 +3805,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2", "thiserror 2.0.18", @@ -4089,7 +3825,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "slab", @@ -4128,12 +3864,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.7.3" @@ -4330,6 +4060,15 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -4392,6 +4131,54 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rquickjs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a" +dependencies = [ + "rquickjs-core", + "rquickjs-macro", +] + +[[package]] +name = "rquickjs-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1" +dependencies = [ + "async-lock", + "hashbrown 0.16.1", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-macro" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80" +dependencies = [ + "convert_case 0.10.0", + "fnv", + "ident_case", + "indexmap 2.13.0", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "rquickjs-core", + "syn 2.0.114", +] + +[[package]] +name = "rquickjs-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83" +dependencies = [ + "cc", +] + [[package]] name = "rusqlite" version = "0.31.0" @@ -4463,47 +4250,19 @@ dependencies = [ "url", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "semver", ] [[package]] @@ -4515,7 +4274,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -4682,15 +4441,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.27" @@ -4701,12 +4451,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.228" @@ -4766,7 +4510,6 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -4815,19 +4558,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_v8" -version = "0.223.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf3d859dda87ee96423c01244f10af864fa6d6a9fcdc2b77e0595078ea0ea11" -dependencies = [ - "num-bigint", - "serde", - "smallvec", - "thiserror 1.0.69", - "v8", -] - [[package]] name = "serde_with" version = "3.16.1" @@ -4929,15 +4659,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-abstraction" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" -dependencies = [ - "outref", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -5026,37 +4747,12 @@ dependencies = [ "system-deps", ] -[[package]] -name = "sourcemap" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4" -dependencies = [ - "base64-simd", - "bitvec", - "data-encoding", - "debugid", - "if_chain", - "rustc-hash 1.1.0", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id-start", - "url", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "string_cache" version = "0.8.9" @@ -5088,28 +4784,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.114", -] - [[package]] name = "subtle" version = "2.6.1" @@ -5263,12 +4937,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" @@ -5339,7 +5007,7 @@ dependencies = [ "heck 0.5.0", "json-patch", "schemars 0.8.22", - "semver 1.0.27", + "semver", "serde", "serde_json", "tauri-utils", @@ -5362,7 +5030,7 @@ dependencies = [ "png", "proc-macro2", "quote", - "semver 1.0.27", + "semver", "serde", "serde_json", "sha2", @@ -5576,7 +5244,7 @@ dependencies = [ "quote", "regex", "schemars 0.8.22", - "semver 1.0.27", + "semver", "serde", "serde-untagged", "serde_json", @@ -5656,7 +5324,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.3", + "rustix", "windows-sys 0.61.2", ] @@ -6172,12 +5840,6 @@ dependencies = [ "unic-common", ] -[[package]] -name = "unicode-id-start" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" - [[package]] name = "unicode-ident" version = "1.0.22" @@ -6261,23 +5923,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v8" -version = "0.106.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a381badc47c6f15acb5fe0b5b40234162349ed9d4e4fd7c83a7f5547c0fc69c5" -dependencies = [ - "bindgen 0.69.5", - "bitflags 2.10.0", - "fslock", - "gzip-header", - "home", - "miniz_oxide 0.7.4", - "once_cell", - "paste", - "which 6.0.3", -] - [[package]] name = "valuable" version = "0.1.1" @@ -6552,30 +6197,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] -name = "which" -version = "6.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" -dependencies = [ - "either", - "home", - "rustix 0.38.44", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -7124,12 +6745,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -7187,15 +6802,6 @@ dependencies = [ "x11-dl", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11" version = "2.21.0" @@ -7271,7 +6877,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix", "serde", "serde_repr", "tracing", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7e122f2b8..93239b688 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -93,7 +93,7 @@ tauri-plugin-autostart = "2" tauri-plugin-notification = "2" # V8 JavaScript runtime (via deno_core - powers Deno) # Only available on desktop - prebuilt binaries don't exist for Android/iOS -deno_core = "0.314" +rquickjs = { version = "0.11", features = ["futures", "macro", "loader", "parallel"] } # TDLib Rust bindings (desktop only - downloads prebuilt TDLib library) tdlib-rs = { version = "1.2", features = ["download-tdlib"] } # Local LLM inference - desktop only (llama.cpp requires native C++ compilation) diff --git a/src-tauri/build.rs b/src-tauri/build.rs index f34696619..7cc371b36 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,195 +1,4 @@ fn main() { - // Get the target OS from environment variable (set by Cargo during cross-compilation) - let target = std::env::var("TARGET").unwrap_or_default(); - let is_mobile_target = target.contains("android") || target.contains("ios"); - let is_macos_target = target.contains("apple") && !target.contains("ios"); - - // TDLib build configuration (desktop only) - // The tdlib-rs crate with download-tdlib feature handles downloading and linking - // the prebuilt TDLib library automatically. - // Note: We check the TARGET env var because cfg() checks the HOST platform for build scripts. - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if !is_mobile_target { - // Download and link TDLib library - // Pass None to use default download location - tdlib_rs::build::build(None); - - // On macOS, copy TDLib and its dependencies to libraries/ so that - // tauri.conf.json > bundle > macOS > frameworks can bundle them into - // the .app's Contents/Frameworks/ directory. - if is_macos_target { - prepare_macos_libraries(); - } - } - - // Add @executable_path/../Frameworks to rpath so the binary can find - // bundled dylibs at runtime (macOS only). - if is_macos_target && !is_mobile_target { - println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); - } - + tdlib_rs::build::build(None); tauri_build::build() } - -/// Copy TDLib dylib and its non-system dependencies (OpenSSL) to src-tauri/libraries/ -/// and rewrite their install names to use @rpath so they work inside Contents/Frameworks/. -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn prepare_macos_libraries() { - use std::fs; - use std::os::unix::fs::PermissionsExt; - use std::path::{Path, PathBuf}; - - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); - let tdlib_lib_dir = PathBuf::from(&out_dir).join("tdlib").join("lib"); - let manifest_dir = - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set")); - let dest_dir = manifest_dir.join("libraries"); - - // Create libraries/ directory - fs::create_dir_all(&dest_dir).expect("Failed to create libraries directory"); - - let tdlib_dylib = "libtdjson.1.8.29.dylib"; - let tdlib_src = tdlib_lib_dir.join(tdlib_dylib); - - if !tdlib_src.exists() { - println!( - "cargo:warning=TDLib dylib not found at {}, skipping library preparation", - tdlib_src.display() - ); - return; - } - - // Copy TDLib dylib - let tdlib_dest = dest_dir.join(tdlib_dylib); - fs::copy(&tdlib_src, &tdlib_dest).expect("Failed to copy TDLib dylib"); - make_writable(&tdlib_dest); - - // Fix TDLib's install name - run_install_name_tool(&["-id", &format!("@rpath/{tdlib_dylib}"), tdlib_dest.to_str().unwrap()]); - - // Find and bundle non-system dependencies (e.g. OpenSSL from Homebrew) - let deps = get_non_system_deps(&tdlib_dest); - for dep_path in &deps { - let dep_name = Path::new(dep_path) - .file_name() - .unwrap() - .to_str() - .unwrap(); - let dep_dest = dest_dir.join(dep_name); - - if Path::new(dep_path).exists() { - fs::copy(dep_path, &dep_dest).unwrap_or_else(|e| panic!("Failed to copy {dep_name}: {e}")); - make_writable(&dep_dest); - - // Fix the dependency's install name - run_install_name_tool(&[ - "-id", - &format!("@rpath/{dep_name}"), - dep_dest.to_str().unwrap(), - ]); - - // Update TDLib's reference to this dependency - run_install_name_tool(&[ - "-change", - dep_path, - &format!("@rpath/{dep_name}"), - tdlib_dest.to_str().unwrap(), - ]); - - println!("cargo:warning=Bundled dependency: {dep_name}"); - } else { - println!("cargo:warning=Dependency not found: {dep_path}"); - } - } - - // Fix cross-references between dependencies (e.g. libssl -> libcrypto) - let bundled_libs: Vec = fs::read_dir(&dest_dir) - .unwrap() - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.extension().is_some_and(|ext| ext == "dylib")) - .collect(); - - for lib in &bundled_libs { - let lib_deps = get_non_system_deps(lib); - for dep_path in &lib_deps { - let dep_name = Path::new(dep_path) - .file_name() - .unwrap() - .to_str() - .unwrap(); - // Only fix if the dep is one we bundled - if dest_dir.join(dep_name).exists() { - run_install_name_tool(&[ - "-change", - dep_path, - &format!("@rpath/{dep_name}"), - lib.to_str().unwrap(), - ]); - } - } - } - - println!("cargo:warning=TDLib libraries prepared in libraries/"); -} - -/// Get non-system dependencies of a dylib using otool -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn get_non_system_deps(dylib_path: &std::path::Path) -> Vec { - use std::process::Command; - - let output = Command::new("otool") - .args(["-L", dylib_path.to_str().unwrap()]) - .output() - .expect("Failed to run otool"); - - let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() - .skip(1) // First line is the dylib itself - .filter_map(|line| { - let path = line.trim().split_whitespace().next()?; - // Keep only non-system, non-rpath paths (e.g. Homebrew libs) - if path.starts_with("/usr/lib/") - || path.starts_with("/System/") - || path.starts_with("@rpath/") - || path.starts_with("@executable_path/") - { - None - } else if path.starts_with("/opt/homebrew/") || path.starts_with("/usr/local/") { - Some(path.to_string()) - } else { - None - } - }) - .collect() -} - -/// Make a file writable (Homebrew libs are read-only) -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn make_writable(path: &std::path::Path) { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(path) - .expect("Failed to read file metadata") - .permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(path, perms).expect("Failed to set file permissions"); -} - -/// Run install_name_tool with the given arguments -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn run_install_name_tool(args: &[&str]) { - use std::process::Command; - - let status = Command::new("install_name_tool") - .args(args) - .status() - .expect("Failed to run install_name_tool"); - - if !status.success() { - println!( - "cargo:warning=install_name_tool failed with args: {}", - args.join(" ") - ); - } -} diff --git a/src-tauri/images/background-dmg.png b/src-tauri/images/background-dmg.png index 1c8e0d7bb..0e296d3fe 100644 Binary files a/src-tauri/images/background-dmg.png and b/src-tauri/images/background-dmg.png differ diff --git a/src-tauri/src/commands/runtime.rs b/src-tauri/src/commands/runtime.rs index 52c7d5b72..43fbdd223 100644 --- a/src-tauri/src/commands/runtime.rs +++ b/src-tauri/src/commands/runtime.rs @@ -14,7 +14,7 @@ use tauri::State; // Desktop-only imports #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::runtime::v8_engine::RuntimeEngine; +use crate::runtime::qjs_engine::RuntimeEngine; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::runtime::types::{SkillSnapshot, ToolResult}; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 767b1fa52..a57696dc0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -84,7 +84,7 @@ macro_rules! common_handlers { ai_read_memory_file, ai_write_memory_file, ai_list_memory_files, - // V8 runtime commands + // Runtime commands runtime_discover_skills, runtime_list_skills, runtime_start_skill, @@ -93,14 +93,14 @@ macro_rules! common_handlers { runtime_call_tool, runtime_all_tools, runtime_broadcast_event, - // V8 runtime enable/disable + KV commands + // Runtime enable/disable + KV commands runtime_enable_skill, runtime_disable_skill, runtime_is_skill_enabled, runtime_get_skill_preferences, runtime_skill_kv_get, runtime_skill_kv_set, - // V8 runtime JSON-RPC + data commands + // Runtime JSON-RPC + data commands runtime_rpc, runtime_skill_data_read, runtime_skill_data_write, @@ -207,15 +207,6 @@ fn setup_tray(app: &AppHandle) -> Result<(), Box> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // Load .env file (silently ignore if missing — production won't have one) - // Try current directory first, then parent (for when running from src-tauri) - if dotenvy::dotenv().is_err() { - if let Ok(cwd) = std::env::current_dir() { - if let Some(parent) = cwd.parent() { - let _ = dotenvy::from_path(parent.join(".env")); - } - } - } // Initialize platform-appropriate logger #[cfg(target_os = "android")] @@ -289,7 +280,7 @@ pub fn run() { ); socket_mgr.set_app_handle(app.handle().clone()); - // Initialize V8 Runtime Engine (desktop only - V8 not available on Android/iOS) + // Initialize QuickJS Runtime Engine (desktop only - not available on Android/iOS) #[cfg(not(any(target_os = "android", target_os = "ios")))] { let data_dir = app @@ -308,7 +299,7 @@ pub fn run() { services::llama::LLAMA_MANAGER.set_data_dir(model_dir); log::info!("[runtime] Local model service initialized"); - match runtime::v8_engine::RuntimeEngine::new(skills_data_dir) { + match runtime::qjs_engine::RuntimeEngine::new(skills_data_dir) { Ok(engine) => { engine.set_app_handle(app.handle().clone()); @@ -330,28 +321,29 @@ pub fn run() { cron.start(); }); - // Auto-start skills in background + // Auto-start skills in background (no delay needed for QuickJS - + // lightweight contexts don't have V8's memory reservation issue) let engine_clone = engine.clone(); tauri::async_runtime::spawn(async move { engine_clone.auto_start_skills().await; }); - log::info!("[runtime] V8 runtime engine initialized"); + log::info!("[runtime] QuickJS runtime engine initialized"); } Err(e) => { - log::error!("[runtime] Failed to initialize V8 runtime: {e}"); + log::error!("[runtime] Failed to initialize QuickJS runtime: {e}"); } } } #[cfg(target_os = "android")] { - log::info!("[runtime] V8 runtime and local model disabled on Android"); + log::info!("[runtime] QuickJS runtime and local model disabled on Android"); } #[cfg(target_os = "ios")] { - log::info!("[runtime] V8 runtime and local model disabled on iOS"); + log::info!("[runtime] QuickJS runtime and local model disabled on iOS"); } // Store SocketManager as Tauri state @@ -433,7 +425,7 @@ pub fn run() { ai_read_memory_file, ai_write_memory_file, ai_list_memory_files, - // V8 runtime commands + // Runtime commands runtime_discover_skills, runtime_list_skills, runtime_start_skill, @@ -442,14 +434,14 @@ pub fn run() { runtime_call_tool, runtime_all_tools, runtime_broadcast_event, - // V8 runtime enable/disable + KV commands + // Runtime enable/disable + KV commands runtime_enable_skill, runtime_disable_skill, runtime_is_skill_enabled, runtime_get_skill_preferences, runtime_skill_kv_get, runtime_skill_kv_set, - // V8 runtime JSON-RPC + data commands + // Runtime JSON-RPC + data commands runtime_rpc, runtime_skill_data_read, runtime_skill_data_write, @@ -525,7 +517,7 @@ pub fn run() { ai_read_memory_file, ai_write_memory_file, ai_list_memory_files, - // V8 runtime commands + // Runtime commands runtime_discover_skills, runtime_list_skills, runtime_start_skill, @@ -534,14 +526,14 @@ pub fn run() { runtime_call_tool, runtime_all_tools, runtime_broadcast_event, - // V8 runtime enable/disable + KV commands + // Runtime enable/disable + KV commands runtime_enable_skill, runtime_disable_skill, runtime_is_skill_enabled, runtime_get_skill_preferences, runtime_skill_kv_get, runtime_skill_kv_set, - // V8 runtime JSON-RPC + data commands + // Runtime JSON-RPC + data commands runtime_rpc, runtime_skill_data_read, runtime_skill_data_write, diff --git a/src-tauri/src/runtime/loader.rs b/src-tauri/src/runtime/loader.rs index f65721713..7479035de 100644 --- a/src-tauri/src/runtime/loader.rs +++ b/src-tauri/src/runtime/loader.rs @@ -1,10 +1,9 @@ -//! Custom V8 module resolver and loader for `@alphahuman/*` imports. +//! Custom module resolver and loader for `@alphahuman/*` imports. //! //! NOTE: Currently unused. Skills access bridge APIs via globals (db, store, console) -//! injected by v8_skill_instance.rs. This module is reserved for future ES module +//! injected by qjs_skill_instance.rs. This module is reserved for future ES module //! import support (e.g., `import { db } from '@alphahuman/db'`). //! //! The globals-based approach was chosen because: -//! - V8/deno_core shares the module loader across all contexts in the same runtime -//! - Per-skill module loaders would require separate runtime instances //! - Globals are simpler and sufficient for the initial implementation +//! - Per-skill module loaders can be added later if needed diff --git a/src-tauri/src/runtime/manifest.rs b/src-tauri/src/runtime/manifest.rs index edd1ba2d1..9897cbb17 100644 --- a/src-tauri/src/runtime/manifest.rs +++ b/src-tauri/src/runtime/manifest.rs @@ -86,10 +86,10 @@ impl SkillManifest { Self::from_json(&content) } - /// Whether this manifest declares a JavaScript runtime (V8). - /// Accepts "v8", "javascript" for compatibility. + /// Whether this manifest declares a JavaScript runtime (QuickJS). + /// Accepts "v8", "javascript", "quickjs" for compatibility. pub fn is_javascript(&self) -> bool { - self.runtime == "v8" || self.runtime == "javascript" + matches!(self.runtime.as_str(), "v8" | "javascript" | "quickjs") } /// Whether the skill is available on the current platform. diff --git a/src-tauri/src/runtime/mod.rs b/src-tauri/src/runtime/mod.rs index 720f9d16d..c4acfb5f1 100644 --- a/src-tauri/src/runtime/mod.rs +++ b/src-tauri/src/runtime/mod.rs @@ -1,9 +1,9 @@ -//! V8 skill runtime module. +//! QuickJS skill runtime module. //! //! Provides a persistent JavaScript execution environment for skills -//! using the V8 engine via `deno_core`. +//! using the QuickJS engine via `rquickjs`. //! -//! Note: V8/deno_core is only available on desktop platforms. +//! Note: The skill runtime is only available on desktop platforms. //! On mobile (Android/iOS), the skill runtime is disabled. // Platform-agnostic modules @@ -13,7 +13,7 @@ pub mod preferences; pub mod socket_manager; pub mod types; -// V8/deno_core modules - desktop only (no prebuilt binaries for Android/iOS) +// QuickJS modules - desktop only (not available on Android/iOS) #[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod bridge; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -21,6 +21,6 @@ pub mod cron_scheduler; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod skill_registry; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub mod v8_engine; +pub mod qjs_engine; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub mod v8_skill_instance; +pub mod qjs_skill_instance; diff --git a/src-tauri/src/runtime/v8_engine.rs b/src-tauri/src/runtime/qjs_engine.rs similarity index 93% rename from src-tauri/src/runtime/v8_engine.rs rename to src-tauri/src/runtime/qjs_engine.rs index d54d5f8b9..cada75935 100644 --- a/src-tauri/src/runtime/v8_engine.rs +++ b/src-tauri/src/runtime/qjs_engine.rs @@ -1,7 +1,7 @@ -//! RuntimeEngine — top-level orchestrator for the V8 skill runtime. +//! RuntimeEngine — top-level orchestrator for the QuickJS skill runtime. //! //! Manages skill lifecycle and provides the public API consumed by Tauri commands. -//! Uses V8 (via deno_core) for JavaScript execution. +//! Uses QuickJS (via rquickjs) for JavaScript execution. use std::path::PathBuf; use std::sync::Arc; @@ -14,10 +14,10 @@ use crate::runtime::manifest::SkillManifest; use crate::runtime::preferences::PreferencesStore; use crate::runtime::skill_registry::SkillRegistry; use crate::runtime::types::{events, SkillSnapshot, SkillStatus, ToolResult}; -use crate::runtime::v8_skill_instance::{BridgeDeps, V8SkillInstance}; +use crate::runtime::qjs_skill_instance::{BridgeDeps, QjsSkillInstance}; use crate::services::tdlib_v8::storage::IdbStorage; -/// The central runtime engine using V8. +/// The central runtime engine using QuickJS. pub struct RuntimeEngine { /// Registry of all skills. registry: Arc, @@ -43,7 +43,7 @@ impl RuntimeEngine { cron_scheduler.set_registry(Arc::clone(®istry)); let preferences = Arc::new(PreferencesStore::new(&skills_data_dir)); - log::info!("[runtime] V8 RuntimeEngine created"); + log::info!("[runtime] QuickJS RuntimeEngine created"); Ok(Self { registry, @@ -111,9 +111,7 @@ impl RuntimeEngine { } // 4. Production: bundled resources - // Tauri converts "../" to "_up_/" when bundling resources with array notation if let Some(resource_dir) = self.resource_dir.read().as_ref() { - // Try: $RESOURCE/_up_/skills/skills/ (array notation with ../) let bundled_skills = resource_dir.join("_up_").join("skills").join("skills"); if bundled_skills.exists() { log::info!( @@ -123,7 +121,6 @@ impl RuntimeEngine { return Ok(bundled_skills); } - // Try: $RESOURCE/skills/ (map notation) let bundled_skills_alt = resource_dir.join("skills"); if bundled_skills_alt.exists() { log::info!( @@ -140,7 +137,7 @@ impl RuntimeEngine { ); } - // 5. Final fallback: app data dir (for user-installed skills) + // 5. Final fallback: app data dir let prod_dir = self.skills_data_dir.clone(); log::info!( "[runtime] Skills source dir (data dir fallback): {:?}", @@ -184,7 +181,6 @@ impl RuntimeEngine { ); } Ok(_) => { - // Not a JavaScript skill, skip log::info!( "[runtime] Skipping skill '{}': not a JavaScript skill", manifest_path.display() @@ -227,7 +223,7 @@ impl RuntimeEngine { let manifest = SkillManifest::from_path(&manifest_path).await?; if !manifest.is_javascript() { return Err(format!( - "Skill '{}' uses runtime '{}', not 'v8' or 'javascript'", + "Skill '{}' uses runtime '{}', not a supported JavaScript runtime", skill_id, manifest.runtime )); } @@ -235,15 +231,15 @@ impl RuntimeEngine { let config = manifest.to_config(); let data_dir = self.skills_data_dir.join(skill_id); - // Create the V8 skill instance - log::info!("[runtime] Creating V8 skill instance for '{}'", skill_id); + // Create the QuickJS skill instance + log::info!("[runtime] Creating QuickJS skill instance for '{}'", skill_id); log::info!("[runtime] Config: {:?}", config); log::info!("[runtime] Skill dir: {:?}", skill_dir); log::info!("[runtime] Data dir: {:?}", data_dir); - let (instance, rx) = V8SkillInstance::new(config.clone(), skill_dir, data_dir.clone()); - log::info!("[runtime] V8 skill instance created for '{}'", skill_id); + let (instance, rx) = QjsSkillInstance::new(config.clone(), skill_dir, data_dir.clone()); + log::info!("[runtime] QuickJS skill instance created for '{}'", skill_id); - // Bundle bridge dependencies + // Bundle bridge dependencies (no creation lock needed for QuickJS) let deps = BridgeDeps { cron_scheduler: self.cron_scheduler.clone(), skill_registry: self.registry.clone(), @@ -265,8 +261,7 @@ impl RuntimeEngine { self.emit_status_change(skill_id); - // Wait for initialization to complete (Running or Error status) - // with a reasonable timeout + // Wait for initialization to complete let state = instance.state.clone(); let skill_id_owned = skill_id.to_string(); let max_wait = std::time::Duration::from_secs(10); @@ -278,12 +273,10 @@ impl RuntimeEngine { match current_status { SkillStatus::Running => { - // Successfully started self.emit_status_change(&skill_id_owned); return Ok(instance.snapshot()); } SkillStatus::Error => { - // Initialization failed - unregister and return error let error_msg = state .read() .error @@ -297,7 +290,6 @@ impl RuntimeEngine { )); } SkillStatus::Stopped => { - // Skill stopped unexpectedly during init self.registry.unregister(&skill_id_owned); return Err(format!( "Skill '{}' stopped unexpectedly during initialization", @@ -305,9 +297,7 @@ impl RuntimeEngine { )); } SkillStatus::Initializing | SkillStatus::Pending => { - // Still initializing, continue waiting if start.elapsed() > max_wait { - // Timeout - skill is taking too long log::warn!( "[runtime] Skill '{}' initialization timeout, returning current state", skill_id_owned @@ -317,7 +307,6 @@ impl RuntimeEngine { tokio::time::sleep(poll_interval).await; } SkillStatus::Stopping => { - // Unexpected state during startup return Err(format!( "Skill '{}' is in unexpected Stopping state during startup", skill_id_owned @@ -375,6 +364,7 @@ impl RuntimeEngine { } /// Auto-start skills based on user preferences. + /// No stagger delay needed for QuickJS (lightweight contexts). pub async fn auto_start_skills(&self) { match self.discover_skills().await { Ok(manifests) => { diff --git a/src-tauri/src/runtime/qjs_skill_instance.rs b/src-tauri/src/runtime/qjs_skill_instance.rs new file mode 100644 index 000000000..a000c6857 --- /dev/null +++ b/src-tauri/src/runtime/qjs_skill_instance.rs @@ -0,0 +1,649 @@ +//! QjsSkillInstance — manages one QuickJS context per skill. +//! +//! Key differences from V8 version: +//! - QuickJS contexts are Send+Sync with `parallel` feature, so we use regular tokio::spawn (not spawn_blocking) +//! - No V8 creation lock needed (QuickJS contexts are lightweight ~1-2MB) +//! - No stagger delay needed between skill starts +//! - Direct memory limits via `rt.set_memory_limit()` +//! - Uses `ctx.eval::(code)` instead of `runtime.execute_script()` +//! - Simplified error handling with rquickjs::Error + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::RwLock; +use tokio::sync::mpsc; + +use crate::runtime::cron_scheduler::CronScheduler; +use crate::runtime::skill_registry::SkillRegistry; +use crate::runtime::types::{ + SkillConfig, SkillMessage, SkillSnapshot, SkillStatus, ToolContent, ToolDefinition, ToolResult, +}; +use crate::services::tdlib_v8::{qjs_ops, IdbStorage}; + +/// Dependencies passed to a skill instance for bridge installation. +#[allow(dead_code)] +pub struct BridgeDeps { + pub cron_scheduler: Arc, + pub skill_registry: Arc, + pub app_handle: Option, + pub data_dir: PathBuf, + // NOTE: No v8_creation_lock - QuickJS doesn't need it +} + +/// Shared mutable state for a skill instance. +pub struct SkillState { + pub status: SkillStatus, + pub tools: Vec, + pub error: Option, + pub published_state: HashMap, +} + +impl Default for SkillState { + fn default() -> Self { + Self { + status: SkillStatus::Pending, + tools: Vec::new(), + error: None, + published_state: HashMap::new(), + } + } +} + +/// A running skill instance using QuickJS. +pub struct QjsSkillInstance { + pub config: SkillConfig, + pub state: Arc>, + pub sender: mpsc::Sender, + pub skill_dir: PathBuf, + pub data_dir: PathBuf, +} + +impl QjsSkillInstance { + /// Create a new QuickJS skill instance. + pub fn new( + config: SkillConfig, + skill_dir: PathBuf, + data_dir: PathBuf, + ) -> (Self, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(64); + let instance = Self { + config, + state: Arc::new(RwLock::new(SkillState::default())), + sender: tx, + skill_dir, + data_dir, + }; + (instance, rx) + } + + /// Take a snapshot of the current skill state. + pub fn snapshot(&self) -> SkillSnapshot { + let state = self.state.read(); + SkillSnapshot { + skill_id: self.config.skill_id.clone(), + name: self.config.name.clone(), + status: state.status, + tools: state.tools.clone(), + error: state.error.clone(), + state: state.published_state.clone(), + } + } + + /// Spawn the skill's execution loop as a tokio task. + /// Unlike V8 (which needed spawn_blocking), QuickJS contexts are Send. + pub fn spawn( + &self, + mut rx: mpsc::Receiver, + _deps: BridgeDeps, + ) -> tokio::task::JoinHandle<()> { + let config = self.config.clone(); + let state = self.state.clone(); + let skill_dir = self.skill_dir.clone(); + let data_dir = self.data_dir.clone(); + + tokio::spawn(async move { + // Update status + state.write().status = SkillStatus::Initializing; + + // Create storage + let storage = match IdbStorage::new(&data_dir) { + Ok(s) => s, + Err(e) => { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("Failed to create storage: {e}")); + log::error!("[skill:{}] Storage creation failed: {e}", config.skill_id); + return; + } + }; + + // Read the entry point JS file + let entry_path = skill_dir.join(&config.entry_point); + let js_source = match tokio::fs::read_to_string(&entry_path).await { + Ok(src) => src, + Err(e) => { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("Failed to read {}: {e}", config.entry_point)); + log::error!("[skill:{}] Failed to read entry point: {e}", config.skill_id); + return; + } + }; + + // Create QuickJS runtime with memory limits + let rt = match rquickjs::AsyncRuntime::new() { + Ok(rt) => rt, + Err(e) => { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("Failed to create QuickJS runtime: {e}")); + log::error!("[skill:{}] Failed to create QuickJS runtime: {e}", config.skill_id); + return; + } + }; + + // Set memory limit (config.memory_limit is in bytes) + rt.set_memory_limit(config.memory_limit).await; + rt.set_max_stack_size(512 * 1024).await; // 512KB stack + + // Create context with full standard library + let ctx = match rquickjs::AsyncContext::full(&rt).await { + Ok(ctx) => ctx, + Err(e) => { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("Failed to create QuickJS context: {e}")); + log::error!("[skill:{}] Failed to create context: {e}", config.skill_id); + return; + } + }; + + // Create shared timer state + let timer_state = Arc::new(RwLock::new(qjs_ops::TimerState::default())); + + // Create WebSocket state + let ws_state = Arc::new(RwLock::new(qjs_ops::WebSocketState::default())); + + // Create published skill state (different from SkillState above) + let published_state = Arc::new(RwLock::new(qjs_ops::SkillState::default())); + + // Register ops and run bootstrap + skill code + let skill_id = config.skill_id.clone(); + let init_result = ctx.with(|js_ctx| { + // Register native ops as __ops global + let skill_context = qjs_ops::SkillContext { + skill_id: skill_id.clone(), + data_dir: data_dir.clone(), + }; + + if let Err(e) = qjs_ops::register_ops( + &js_ctx, + storage.clone(), + skill_context, + published_state.clone(), + timer_state.clone(), + ws_state.clone(), + ) { + return Err(format!("Failed to register ops: {e}")); + } + + // Load bootstrap + let bootstrap_code = include_str!("../services/tdlib_v8/bootstrap.js"); + if let Err(e) = js_ctx.eval::(bootstrap_code) { + let err_str = format!("Bootstrap failed: {e}"); + return Err(err_str); + } + + // Set skill ID + let bridge_code = format!( + r#"globalThis.__skillId = "{}";"#, + skill_id.replace('"', r#"\""#) + ); + if let Err(e) = js_ctx.eval::(bridge_code.as_bytes()) { + return Err(format!("Skill init failed: {e}")); + } + + // Execute the skill's entry point + if let Err(e) = js_ctx.eval::(js_source.as_bytes()) { + return Err(format!("Skill load failed: {e}")); + } + + // Extract tool definitions + extract_tools(&js_ctx, &state); + + Ok(()) + }).await; + + if let Err(e) = init_result { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(e.clone()); + log::error!("[skill:{}] {e}", config.skill_id); + return; + } + + // Call init() lifecycle + if let Err(e) = call_lifecycle(&ctx, "init").await { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("init() failed: {e}")); + log::error!("[skill:{}] init() failed: {e}", config.skill_id); + return; + } + + // Execute pending jobs after init (process promises) + drive_jobs(&rt).await; + + // Call start() lifecycle + if let Err(e) = call_lifecycle(&ctx, "start").await { + let mut s = state.write(); + s.status = SkillStatus::Error; + s.error = Some(format!("start() failed: {e}")); + log::error!("[skill:{}] start() failed: {e}", config.skill_id); + return; + } + + // Execute pending jobs after start + drive_jobs(&rt).await; + + // Mark as running + state.write().status = SkillStatus::Running; + log::info!("[skill:{}] Running (QuickJS)", config.skill_id); + + // Run the event loop + run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state).await; + }) + } +} + +// ============================================================================ +// Event Loop +// ============================================================================ + +/// The main event loop that drives the QuickJS runtime. +/// This continuously: +/// 1. Polls for ready timers and fires their callbacks +/// 2. Checks for incoming messages (non-blocking) +/// 3. Runs the QuickJS job queue for promises/async ops +/// 4. Sleeps efficiently when idle +async fn run_event_loop( + rt: &rquickjs::AsyncRuntime, + ctx: &rquickjs::AsyncContext, + rx: &mut mpsc::Receiver, + state: &Arc>, + skill_id: &str, + timer_state: &Arc>, +) { + // Maximum sleep duration when no timers are pending + const MAX_IDLE_SLEEP: Duration = Duration::from_millis(100); + // Minimum sleep to prevent busy-spinning + const MIN_SLEEP: Duration = Duration::from_millis(1); + + loop { + // 1. Poll and fire ready timers + let ready_timers = { + let (ready, _next) = qjs_ops::poll_timers(timer_state); + ready + }; + + // Fire timer callbacks in JavaScript + for timer_id in ready_timers { + fire_timer_callback(ctx, timer_id).await; + } + + // 2. Check for incoming messages (non-blocking) + match rx.try_recv() { + Ok(msg) => { + let should_stop = handle_message(ctx, msg, state, skill_id).await; + if should_stop { + break; + } + } + Err(mpsc::error::TryRecvError::Empty) => { + // No message - that's fine + } + Err(mpsc::error::TryRecvError::Disconnected) => { + // Channel closed, exit + log::info!("[skill:{}] Message channel disconnected, stopping", skill_id); + break; + } + } + + // 3. Drive QuickJS job queue (process pending promises) + drive_jobs(rt).await; + + // 4. Calculate sleep duration based on next timer + let sleep_duration = { + let (_, next_timer) = qjs_ops::poll_timers(timer_state); + match next_timer { + Some(d) if d < MIN_SLEEP => MIN_SLEEP, + Some(d) if d > MAX_IDLE_SLEEP => MAX_IDLE_SLEEP, + Some(d) => d, + None => MAX_IDLE_SLEEP, + } + }; + + // Sleep efficiently - this yields the thread when no work is needed + tokio::time::sleep(sleep_duration).await; + } +} + +/// Drive the QuickJS job queue until no more jobs are pending. +async fn drive_jobs(rt: &rquickjs::AsyncRuntime) { + // idle() runs all pending futures and jobs + rt.idle().await; +} + +/// Fire a timer callback in JavaScript. +async fn fire_timer_callback(ctx: &rquickjs::AsyncContext, timer_id: u32) { + let code = format!("globalThis.__handleTimer({});", timer_id); + ctx.with(|js_ctx| { + if let Err(e) = js_ctx.eval::(code.as_bytes()) { + log::error!("[timer] Callback for timer {} failed: {}", timer_id, e); + } + }).await; +} + +/// Handle a single message from the channel. +/// Returns true if the skill should stop. +async fn handle_message( + ctx: &rquickjs::AsyncContext, + msg: SkillMessage, + state: &Arc>, + skill_id: &str, +) -> bool { + match msg { + SkillMessage::CallTool { tool_name, arguments, reply } => { + let result = handle_tool_call(ctx, &tool_name, arguments).await; + let _ = reply.send(result); + } + SkillMessage::ServerEvent { event, data } => { + let _ = handle_server_event(ctx, &event, data).await; + } + SkillMessage::CronTrigger { schedule_id } => { + let _ = handle_cron_trigger(ctx, &schedule_id).await; + } + SkillMessage::Stop { reply } => { + let _ = call_lifecycle(ctx, "stop").await; + state.write().status = SkillStatus::Stopped; + log::info!("[skill:{}] Stopped", skill_id); + let _ = reply.send(()); + return true; // Signal to stop + } + SkillMessage::SetupStart { reply } => { + let result = handle_js_call(ctx, "onSetupStart", "{}").await; + let _ = reply.send(result); + } + SkillMessage::SetupSubmit { step_id, values, reply } => { + let args = serde_json::json!({ "stepId": step_id, "values": values }); + let result = handle_js_call(ctx, "onSetupSubmit", &args.to_string()).await; + let _ = reply.send(result); + } + SkillMessage::SetupCancel { reply } => { + let result = handle_js_void_call(ctx, "onSetupCancel", "{}").await; + let _ = reply.send(result); + } + SkillMessage::ListOptions { reply } => { + let result = handle_js_call(ctx, "onListOptions", "{}").await; + let _ = reply.send(result); + } + SkillMessage::SetOption { name, value, reply } => { + let args = serde_json::json!({ "name": name, "value": value }); + let result = handle_js_void_call(ctx, "onSetOption", &args.to_string()).await; + let _ = reply.send(result); + } + SkillMessage::SessionStart { session_id, reply } => { + let args = serde_json::json!({ "sessionId": session_id }); + let result = handle_js_void_call(ctx, "onSessionStart", &args.to_string()).await; + let _ = reply.send(result); + } + SkillMessage::SessionEnd { session_id, reply } => { + let args = serde_json::json!({ "sessionId": session_id }); + let result = handle_js_void_call(ctx, "onSessionEnd", &args.to_string()).await; + let _ = reply.send(result); + } + SkillMessage::Tick { reply } => { + let result = handle_js_void_call(ctx, "onTick", "{}").await; + let _ = reply.send(result); + } + SkillMessage::Rpc { method, params, reply } => { + let args = serde_json::json!({ "method": method, "params": params }); + let result = handle_js_call(ctx, "onRpc", &args.to_string()).await; + let _ = reply.send(result); + } + } + false // Don't stop +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Call a lifecycle function on the skill object. +/// Looks for the skill at globalThis.__skill.default first, then falls back to globalThis. +async fn call_lifecycle(ctx: &rquickjs::AsyncContext, name: &str) -> Result<(), String> { + let name = name.to_string(); + ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || globalThis); + if (typeof skill.{name} === 'function') {{ + skill.{name}(); + }} else if (typeof globalThis.{name} === 'function') {{ + globalThis.{name}(); + }} + }})()"# + ); + js_ctx.eval::(code.as_bytes()) + .map_err(|e| format!("{name}() failed: {e}"))?; + Ok(()) + }).await +} + +/// Extract tool definitions from the skill. +fn extract_tools(js_ctx: &rquickjs::Ctx<'_>, state: &Arc>) { + let code = r#" + (function() { + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || null); + var tools = (skill && skill.tools) || globalThis.tools || []; + return JSON.stringify(tools.map(function(t) { + return { + name: t.name || "", + description: t.description || "", + input_schema: t.inputSchema || t.input_schema || {} + }; + })); + })() + "#; + + // eval with String type hint tells rquickjs to convert the result to a Rust String + match js_ctx.eval::(code) { + Ok(json_str) => { + match serde_json::from_str::>(&json_str) { + Ok(tools) => { + state.write().tools = tools; + } + Err(e) => { + log::warn!("[tools] Failed to parse tools JSON: {e}"); + } + } + } + Err(e) => { + log::warn!("[tools] Failed to extract tools: {e}"); + } + } +} + +/// Handle a tool call. +async fn handle_tool_call( + ctx: &rquickjs::AsyncContext, + tool_name: &str, + arguments: serde_json::Value, +) -> Result { + let args_str = serde_json::to_string(&arguments) + .map_err(|e| format!("Failed to serialize args: {e}"))?; + let tool_name = tool_name.to_string(); + + let result_text = ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || null); + var tools = (skill && skill.tools) || globalThis.tools || []; + for (var i = 0; i < tools.length; i++) {{ + if (tools[i].name === "{}") {{ + var args = {}; + var result = tools[i].execute(args); + if (result && typeof result === 'object') {{ + return JSON.stringify(result); + }} + return String(result); + }} + }} + throw new Error("Tool '{}' not found"); + }})()"#, + tool_name.replace('"', r#"\""#), + args_str, + tool_name.replace('"', r#"\""#), + ); + + match js_ctx.eval::(code.as_bytes()) { + Ok(s) => Ok(s), + Err(e) => Err(format!("Tool execution failed: {e}")) + } + }).await?; + + Ok(ToolResult { + content: vec![ToolContent::Text { text: result_text }], + is_error: false, + }) +} + +/// Handle a server event. +async fn handle_server_event( + ctx: &rquickjs::AsyncContext, + event: &str, + data: serde_json::Value, +) -> Result<(), String> { + let data_str = serde_json::to_string(&data).unwrap_or_else(|_| "null".to_string()); + let event = event.to_string(); + + ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || globalThis); + if (typeof skill.onServerEvent === 'function') {{ + skill.onServerEvent("{}", {}); + }} else if (typeof globalThis.onServerEvent === 'function') {{ + globalThis.onServerEvent("{}", {}); + }} + }})()"#, + event.replace('"', r#"\""#), + data_str, + event.replace('"', r#"\""#), + data_str, + ); + + js_ctx.eval::(code.as_bytes()) + .map_err(|e| format!("Event handler failed: {e}"))?; + Ok(()) + }).await +} + +/// Handle a cron trigger. +async fn handle_cron_trigger(ctx: &rquickjs::AsyncContext, schedule_id: &str) -> Result<(), String> { + let schedule_id = schedule_id.to_string(); + ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || globalThis); + if (typeof skill.onCronTrigger === 'function') {{ + skill.onCronTrigger("{}"); + }} else if (typeof globalThis.onCronTrigger === 'function') {{ + globalThis.onCronTrigger("{}"); + }} + }})()"#, + schedule_id.replace('"', r#"\""#), + schedule_id.replace('"', r#"\""#), + ); + js_ctx.eval::(code.as_bytes()) + .map_err(|e| format!("Cron trigger failed: {e}")) + .map(|_| ()) + }).await +} + +/// Call a JS function on the skill object that returns a JSON value. +async fn handle_js_call( + ctx: &rquickjs::AsyncContext, + fn_name: &str, + args_json: &str, +) -> Result { + let fn_name = fn_name.to_string(); + let args_json = args_json.to_string(); + + let result_text = ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || globalThis); + var fn = skill.{fn_name} || globalThis.{fn_name}; + if (typeof fn === 'function') {{ + var args = {args_json}; + var result = fn.call(skill, args); + return JSON.stringify(result); + }} + return "null"; + }})()"# + ); + + match js_ctx.eval::(code.as_bytes()) { + Ok(s) => Ok(s), + Err(e) => Err(format!("{fn_name}() failed: {e}")) + } + }).await?; + + serde_json::from_str(&result_text) + .map_err(|e| format!("{fn_name}() returned invalid JSON: {e}")) +} + +/// Call a JS function on the skill object that returns void. +async fn handle_js_void_call( + ctx: &rquickjs::AsyncContext, + fn_name: &str, + args_json: &str, +) -> Result<(), String> { + let fn_name = fn_name.to_string(); + let args_json = args_json.to_string(); + + ctx.with(|js_ctx| { + let code = format!( + r#"(function() {{ + var skill = globalThis.__skill && globalThis.__skill.default + ? globalThis.__skill.default + : (globalThis.__skill || globalThis); + var fn = skill.{fn_name} || globalThis.{fn_name}; + if (typeof fn === 'function') {{ + var args = {args_json}; + fn.call(skill, args); + }} + }})()"# + ); + + js_ctx.eval::(code.as_bytes()) + .map_err(|e| format!("{fn_name}() failed: {e}")) + .map(|_| ()) + }).await +} diff --git a/src-tauri/src/runtime/skill_registry.rs b/src-tauri/src/runtime/skill_registry.rs index 96dc5e67a..7584a39d9 100644 --- a/src-tauri/src/runtime/skill_registry.rs +++ b/src-tauri/src/runtime/skill_registry.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use parking_lot::RwLock; use tokio::sync::{mpsc, oneshot}; -use crate::runtime::v8_skill_instance::SkillState; +use crate::runtime::qjs_skill_instance::SkillState; use crate::runtime::types::{ SkillConfig, SkillMessage, SkillSnapshot, SkillStatus, ToolDefinition, ToolResult, }; diff --git a/src-tauri/src/runtime/socket_manager.rs b/src-tauri/src/runtime/socket_manager.rs index f2f043738..a8e9ce957 100644 --- a/src-tauri/src/runtime/socket_manager.rs +++ b/src-tauri/src/runtime/socket_manager.rs @@ -30,7 +30,7 @@ use rust_socketio::{ Event, Payload, }; -// SkillRegistry only available on desktop (V8/deno_core required) +// SkillRegistry only available on desktop (QuickJS required) #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::runtime::skill_registry::SkillRegistry; diff --git a/src-tauri/src/runtime/v8_skill_instance.rs b/src-tauri/src/runtime/v8_skill_instance.rs deleted file mode 100644 index 13c516c65..000000000 --- a/src-tauri/src/runtime/v8_skill_instance.rs +++ /dev/null @@ -1,712 +0,0 @@ -//! V8SkillInstance — manages one V8 context per skill. -//! -//! Each skill runs on its own dedicated thread (V8's JsRuntime is not Send) -//! with: -//! - A scoped SQLite database -//! - Bridge globals (db, store, net, platform, console) -//! - An async event loop that drives timers, promises, and handles messages -//! - Lifecycle hooks: init() -> start() -> [event loop] -> stop() - -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; - -use deno_core::{v8, JsRuntime, PollEventLoopOptions, RuntimeOptions}; -use parking_lot::RwLock; -use tokio::sync::mpsc; - -use crate::runtime::cron_scheduler::CronScheduler; -use crate::runtime::skill_registry::SkillRegistry; -use crate::runtime::types::{ - SkillConfig, SkillMessage, SkillSnapshot, SkillStatus, ToolContent, ToolDefinition, ToolResult, -}; -use crate::services::tdlib_v8::{ops, IdbStorage}; - -/// Dependencies passed to a skill instance for bridge installation. -/// Currently not all fields are used, but they're kept for future feature parity. -#[allow(dead_code)] -pub struct BridgeDeps { - pub cron_scheduler: Arc, - pub skill_registry: Arc, - pub app_handle: Option, - pub data_dir: PathBuf, -} - -/// Shared mutable state for a skill instance. -pub struct SkillState { - pub status: SkillStatus, - pub tools: Vec, - pub error: Option, - pub published_state: HashMap, -} - -impl Default for SkillState { - fn default() -> Self { - Self { - status: SkillStatus::Pending, - tools: Vec::new(), - error: None, - published_state: HashMap::new(), - } - } -} - -/// A running skill instance using V8. -pub struct V8SkillInstance { - pub config: SkillConfig, - pub state: Arc>, - pub sender: mpsc::Sender, - pub skill_dir: PathBuf, - pub data_dir: PathBuf, -} - -impl V8SkillInstance { - /// Create a new V8 skill instance. - pub fn new( - config: SkillConfig, - skill_dir: PathBuf, - data_dir: PathBuf, - ) -> (Self, mpsc::Receiver) { - let (tx, rx) = mpsc::channel(64); - let instance = Self { - config, - state: Arc::new(RwLock::new(SkillState::default())), - sender: tx, - skill_dir, - data_dir, - }; - (instance, rx) - } - - /// Take a snapshot of the current skill state. - pub fn snapshot(&self) -> SkillSnapshot { - let state = self.state.read(); - SkillSnapshot { - skill_id: self.config.skill_id.clone(), - name: self.config.name.clone(), - status: state.status, - tools: state.tools.clone(), - error: state.error.clone(), - state: state.published_state.clone(), - } - } - - /// Spawn the skill's execution loop in a dedicated thread. - /// Returns a JoinHandle wrapped in a tokio task for compatibility. - pub fn spawn( - &self, - mut rx: mpsc::Receiver, - _deps: BridgeDeps, - ) -> tokio::task::JoinHandle<()> { - let config = self.config.clone(); - let state = self.state.clone(); - let skill_dir = self.skill_dir.clone(); - let data_dir = self.data_dir.clone(); - - // Use std::thread::spawn since JsRuntime is not Send - // Wrap in tokio task for API compatibility - tokio::task::spawn_blocking(move || { - // Update status - state.write().status = SkillStatus::Initializing; - - // Create storage - let storage = match IdbStorage::new(&data_dir) { - Ok(s) => s, - Err(e) => { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Failed to create storage: {e}")); - log::error!("[skill:{}] Storage creation failed: {e}", config.skill_id); - return; - } - }; - - // Read the entry point JS file synchronously - let entry_path = skill_dir.join(&config.entry_point); - let js_source = match std::fs::read_to_string(&entry_path) { - Ok(src) => src, - Err(e) => { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Failed to read {}: {e}", config.entry_point)); - log::error!("[skill:{}] Failed to read entry point: {e}", config.skill_id); - return; - } - }; - - // Create V8 runtime with memory limits to prevent OOM crashes. - // Each skill gets its own V8 isolate; without limits V8 tries to reserve - // ~1.4 GB per isolate which can exhaust memory when multiple skills start. - let extension = ops::build_extension(storage.clone()); - let create_params = v8::CreateParams::default() - .heap_limits(0, config.memory_limit); - let mut runtime = match JsRuntime::try_new(RuntimeOptions { - extensions: vec![extension], - create_params: Some(create_params), - ..Default::default() - }) { - Ok(rt) => rt, - Err(e) => { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Failed to create V8 runtime: {e}")); - log::error!( - "[skill:{}] Failed to create V8 runtime (memory_limit={}): {e}", - config.skill_id, - config.memory_limit - ); - return; - } - }; - - // Set skill context in op state - { - let op_state = runtime.op_state(); - let mut state_ref = op_state.borrow_mut(); - ops::init_state_with_data_dir( - &mut state_ref, - storage, - config.skill_id.clone(), - data_dir.clone(), - state.clone(), - ); - } - - // Load bootstrap - let bootstrap_code = include_str!("../services/tdlib_v8/bootstrap.js"); - if let Err(e) = runtime.execute_script("", bootstrap_code.to_string()) { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Bootstrap failed: {e}")); - log::error!("[skill:{}] Bootstrap failed: {e}", config.skill_id); - return; - } - - // Install skill-specific bridges - let skill_id = config.skill_id.clone(); - let bridge_code = format!( - r#"globalThis.__skillId = "{}";"#, - skill_id.replace('"', r#"\""#) - ); - - if let Err(e) = runtime.execute_script("", bridge_code) { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Skill init failed: {e}")); - return; - } - - // Execute the skill's entry point - // Use a static string for the filename - let filename: &'static str = Box::leak(format!("", config.skill_id).into_boxed_str()); - if let Err(e) = runtime.execute_script(filename, js_source) { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("Skill load failed: {e}")); - log::error!("[skill:{}] Load failed: {e}", config.skill_id); - return; - } - - // Extract tool definitions - extract_tools(&mut runtime, &state); - - // Create a tokio runtime for this thread FIRST - all async ops must run inside it - // This is critical because deno_unsync requires CurrentThread runtime for async ops - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to create tokio runtime"); - - // Run the entire lifecycle inside the CurrentThread runtime - rt.block_on(async { - // Call init() - if let Err(e) = call_lifecycle_fn_async(&mut runtime, "init").await { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("init() failed: {e}")); - log::error!("[skill:{}] init() failed: {e}", config.skill_id); - return; - } - - // Call start() - if let Err(e) = call_lifecycle_fn_async(&mut runtime, "start").await { - let mut s = state.write(); - s.status = SkillStatus::Error; - s.error = Some(format!("start() failed: {e}")); - log::error!("[skill:{}] start() failed: {e}", config.skill_id); - return; - } - - // Mark as running - state.write().status = SkillStatus::Running; - log::info!("[skill:{}] Running (V8)", config.skill_id); - - // Run the event loop - run_event_loop(&mut runtime, &mut rx, &state, &config.skill_id).await; - }); - }) - } -} - -// ============================================================================ -// Event Loop -// ============================================================================ - -/// The main event loop that drives the V8 runtime. -/// This continuously: -/// 1. Polls for ready timers and fires their callbacks -/// 2. Checks for incoming messages (non-blocking) -/// 3. Runs the V8 event loop for promises/async ops -/// 4. Sleeps efficiently when idle -async fn run_event_loop( - runtime: &mut JsRuntime, - rx: &mut mpsc::Receiver, - state: &Arc>, - skill_id: &str, -) { - // Maximum sleep duration when no timers are pending - const MAX_IDLE_SLEEP: Duration = Duration::from_millis(100); - // Minimum sleep to prevent busy-spinning - const MIN_SLEEP: Duration = Duration::from_millis(1); - - loop { - // 1. Poll and fire ready timers - let ready_timers = { - let op_state = runtime.op_state(); - let mut state_ref = op_state.borrow_mut(); - let (ready, _next) = ops::poll_timers(&mut state_ref); - ready - }; - - // Fire timer callbacks in JavaScript - for timer_id in ready_timers { - fire_timer_callback(runtime, timer_id); - } - - // 2. Check for incoming messages (non-blocking) - match rx.try_recv() { - Ok(msg) => { - let should_stop = handle_message(runtime, msg, state, skill_id).await; - if should_stop { - break; - } - } - Err(mpsc::error::TryRecvError::Empty) => { - // No message - that's fine - } - Err(mpsc::error::TryRecvError::Disconnected) => { - // Channel closed, exit - log::info!("[skill:{}] Message channel disconnected, stopping", skill_id); - break; - } - } - - // 3. Run the V8 event loop (processes promises, async ops, etc.) - // Use poll mode - returns immediately if nothing to do - let poll_result = runtime - .run_event_loop(PollEventLoopOptions { - wait_for_inspector: false, - pump_v8_message_loop: true, - }) - .await; - - if let Err(e) = poll_result { - log::error!("[skill:{}] Event loop error: {}", skill_id, e); - // Don't break - try to continue - } - - // 4. Calculate sleep duration based on next timer - let sleep_duration = { - let op_state = runtime.op_state(); - let mut state_ref = op_state.borrow_mut(); - let (_, next_timer) = ops::poll_timers(&mut state_ref); - match next_timer { - Some(d) if d < MIN_SLEEP => MIN_SLEEP, - Some(d) if d > MAX_IDLE_SLEEP => MAX_IDLE_SLEEP, - Some(d) => d, - None => MAX_IDLE_SLEEP, - } - }; - - // Sleep efficiently - this yields the thread when no work is needed - tokio::time::sleep(sleep_duration).await; - } -} - -/// Fire a timer callback in JavaScript. -fn fire_timer_callback(runtime: &mut JsRuntime, timer_id: u32) { - let code = format!("globalThis.__handleTimer({});", timer_id); - if let Err(e) = runtime.execute_script("", code) { - log::error!("[timer] Callback for timer {} failed: {}", timer_id, e); - } -} - -/// Handle a single message from the channel. -/// Returns true if the skill should stop. -async fn handle_message( - runtime: &mut JsRuntime, - msg: SkillMessage, - state: &Arc>, - skill_id: &str, -) -> bool { - match msg { - SkillMessage::CallTool { - tool_name, - arguments, - reply, - } => { - let result = handle_tool_call_sync(runtime, &tool_name, arguments); - let _ = reply.send(result); - } - SkillMessage::ServerEvent { event, data } => { - let _ = handle_server_event_sync(runtime, &event, data); - } - SkillMessage::CronTrigger { schedule_id } => { - let _ = handle_cron_trigger_sync(runtime, &schedule_id); - } - SkillMessage::Stop { reply } => { - let _ = call_lifecycle_fn_sync(runtime, "stop"); - state.write().status = SkillStatus::Stopped; - log::info!("[skill:{}] Stopped", skill_id); - let _ = reply.send(()); - return true; // Signal to stop - } - SkillMessage::SetupStart { reply } => { - let result = handle_js_call_sync(runtime, "onSetupStart", "{}"); - let _ = reply.send(result); - } - SkillMessage::SetupSubmit { - step_id, - values, - reply, - } => { - let args = serde_json::json!({ - "stepId": step_id, - "values": values, - }); - let result = handle_js_call_sync(runtime, "onSetupSubmit", &args.to_string()); - let _ = reply.send(result); - } - SkillMessage::SetupCancel { reply } => { - let result = handle_js_void_call_sync(runtime, "onSetupCancel", "{}"); - let _ = reply.send(result); - } - SkillMessage::ListOptions { reply } => { - let result = handle_js_call_sync(runtime, "onListOptions", "{}"); - let _ = reply.send(result); - } - SkillMessage::SetOption { name, value, reply } => { - let args = serde_json::json!({ - "name": name, - "value": value, - }); - let result = handle_js_void_call_sync(runtime, "onSetOption", &args.to_string()); - let _ = reply.send(result); - } - SkillMessage::SessionStart { session_id, reply } => { - let args = serde_json::json!({ "sessionId": session_id }); - let result = handle_js_void_call_sync(runtime, "onSessionStart", &args.to_string()); - let _ = reply.send(result); - } - SkillMessage::SessionEnd { session_id, reply } => { - let args = serde_json::json!({ "sessionId": session_id }); - let result = handle_js_void_call_sync(runtime, "onSessionEnd", &args.to_string()); - let _ = reply.send(result); - } - SkillMessage::Tick { reply } => { - let result = handle_js_void_call_sync(runtime, "onTick", "{}"); - let _ = reply.send(result); - } - SkillMessage::Rpc { - method, - params, - reply, - } => { - let args = serde_json::json!({ - "method": method, - "params": params, - }); - let result = handle_js_call_sync(runtime, "onRpc", &args.to_string()); - let _ = reply.send(result); - } - } - false // Don't stop -} - -/// Extract tool definitions from skill.tools (supports both globalThis.__skill.default and globalThis.tools). -fn extract_tools(runtime: &mut JsRuntime, state: &Arc>) { - let code = r#" - (function() { - // Try to get skill from bundled export first, then fall back to globalThis.tools - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || null); - var tools = (skill && skill.tools) || globalThis.tools || []; - return JSON.stringify(tools.map(function(t) { - return { - name: t.name || "", - description: t.description || "", - input_schema: t.inputSchema || t.input_schema || {} - }; - })); - })() - "#; - - if let Ok(result) = runtime.execute_script("", code.to_string()) { - let scope = &mut runtime.handle_scope(); - let local = v8::Local::new(scope, result); - - if let Some(s) = local.to_string(scope) { - let json_str = s.to_rust_string_lossy(scope); - if let Ok(tools) = serde_json::from_str::>(&json_str) { - state.write().tools = tools; - } - } - } -} - -/// Call a lifecycle function on the skill object asynchronously. -/// This version runs inside the skill's CurrentThread runtime - no nested runtime creation. -/// Looks for the skill at globalThis.__skill.default first, then falls back to globalThis. -/// -/// Note: This does NOT wait for the event loop to complete, because lifecycle functions -/// may start async operations (like update loops) that run indefinitely. The main event -/// loop will process pending async work after init/start complete. -async fn call_lifecycle_fn_async(runtime: &mut JsRuntime, name: &str) -> Result<(), String> { - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - if (typeof skill.{name} === 'function') {{ - skill.{name}(); - }} else if (typeof globalThis.{name} === 'function') {{ - globalThis.{name}(); - }} - }})()"# - ); - - runtime - .execute_script("", code) - .map_err(|e| format!("{name}() failed: {e}"))?; - - // Don't wait for event loop here - the main event loop will handle pending async work. - // This is important because lifecycle functions may start long-running async operations - // (like TDLib update loops) that would block init/start from completing. - - Ok(()) -} - -/// Call a lifecycle function on the skill object synchronously. -/// WARNING: This creates its own tokio runtime - only use when NOT inside an async context. -/// Looks for the skill at globalThis.__skill.default first, then falls back to globalThis. -#[allow(dead_code)] -fn call_lifecycle_fn_sync(runtime: &mut JsRuntime, name: &str) -> Result<(), String> { - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - if (typeof skill.{name} === 'function') {{ - skill.{name}(); - }} else if (typeof globalThis.{name} === 'function') {{ - globalThis.{name}(); - }} - }})()"# - ); - - runtime - .execute_script("", code) - .map_err(|e| format!("{name}() failed: {e}"))?; - - // Run event loop synchronously to handle any pending ops - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| format!("Failed to create runtime: {e}"))?; - - rt.block_on(async { - runtime - .run_event_loop(PollEventLoopOptions::default()) - .await - .map_err(|e| format!("Event loop error: {e}")) - })?; - - Ok(()) -} - -/// Handle a tool call synchronously (no async ops waited). -/// This is used when we just need to invoke the tool and get immediate result. -fn handle_tool_call_sync( - runtime: &mut JsRuntime, - tool_name: &str, - arguments: serde_json::Value, -) -> Result { - let args_str = - serde_json::to_string(&arguments).map_err(|e| format!("Failed to serialize args: {e}"))?; - - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || null); - var tools = (skill && skill.tools) || globalThis.tools || []; - for (var i = 0; i < tools.length; i++) {{ - if (tools[i].name === "{}") {{ - var args = {}; - var result = tools[i].execute(args); - if (result && typeof result === 'object') {{ - return JSON.stringify(result); - }} - return String(result); - }} - }} - throw new Error("Tool '{}' not found"); - }})()"#, - tool_name.replace('"', r#"\""#), - args_str, - tool_name.replace('"', r#"\""#), - ); - - let result = runtime - .execute_script("", code) - .map_err(|e| format!("Tool execution failed: {e}"))?; - - // Note: We don't run the event loop here because we're already inside - // the skill's event loop. Pending async ops will be handled by the main loop. - - let scope = &mut runtime.handle_scope(); - let local = v8::Local::new(scope, result); - - let result_text = if let Some(s) = local.to_string(scope) { - s.to_rust_string_lossy(scope) - } else { - "null".to_string() - }; - - Ok(ToolResult { - content: vec![ToolContent::Text { text: result_text }], - is_error: false, - }) -} - -/// Handle a server event synchronously. -fn handle_server_event_sync( - runtime: &mut JsRuntime, - event: &str, - data: serde_json::Value, -) -> Result<(), String> { - let data_str = serde_json::to_string(&data).unwrap_or_else(|_| "null".to_string()); - - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - if (typeof skill.onServerEvent === 'function') {{ - skill.onServerEvent("{}", {}); - }} else if (typeof globalThis.onServerEvent === 'function') {{ - globalThis.onServerEvent("{}", {}); - }} - }})()"#, - event.replace('"', r#"\""#), - data_str, - event.replace('"', r#"\""#), - data_str, - ); - - runtime - .execute_script("", code) - .map_err(|e| format!("Event handler failed: {e}"))?; - - Ok(()) -} - -/// Handle a cron trigger synchronously. -fn handle_cron_trigger_sync(runtime: &mut JsRuntime, schedule_id: &str) -> Result<(), String> { - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - if (typeof skill.onCronTrigger === 'function') {{ - skill.onCronTrigger("{}"); - }} else if (typeof globalThis.onCronTrigger === 'function') {{ - globalThis.onCronTrigger("{}"); - }} - }})()"#, - schedule_id.replace('"', r#"\""#), - schedule_id.replace('"', r#"\""#), - ); - - runtime - .execute_script("", code) - .map_err(|e| format!("Cron trigger failed: {e}"))?; - - Ok(()) -} - -/// Call a JS function on the skill object that returns a JSON value synchronously. -fn handle_js_call_sync( - runtime: &mut JsRuntime, - fn_name: &str, - args_json: &str, -) -> Result { - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - var fn = skill.{fn_name} || globalThis.{fn_name}; - if (typeof fn === 'function') {{ - var args = {args_json}; - var result = fn.call(skill, args); - return JSON.stringify(result); - }} - return "null"; - }})()"# - ); - - let result = runtime - .execute_script("", code) - .map_err(|e| format!("{fn_name}() failed: {e}"))?; - - let scope = &mut runtime.handle_scope(); - let local = v8::Local::new(scope, result); - - let result_text = if let Some(s) = local.to_string(scope) { - s.to_rust_string_lossy(scope) - } else { - "null".to_string() - }; - - serde_json::from_str(&result_text) - .map_err(|e| format!("{fn_name}() returned invalid JSON: {e}")) -} - -/// Call a JS function on the skill object that returns void synchronously. -fn handle_js_void_call_sync( - runtime: &mut JsRuntime, - fn_name: &str, - args_json: &str, -) -> Result<(), String> { - let code = format!( - r#"(function() {{ - var skill = globalThis.__skill && globalThis.__skill.default - ? globalThis.__skill.default - : (globalThis.__skill || globalThis); - var fn = skill.{fn_name} || globalThis.{fn_name}; - if (typeof fn === 'function') {{ - var args = {args_json}; - fn.call(skill, args); - }} - }})()"# - ); - - runtime - .execute_script("", code) - .map_err(|e| format!("{fn_name}() failed: {e}"))?; - - Ok(()) -} diff --git a/src-tauri/src/services/tdlib_v8/bootstrap.js b/src-tauri/src/services/tdlib_v8/bootstrap.js index d8a2fe9c8..2662db39f 100644 --- a/src-tauri/src/services/tdlib_v8/bootstrap.js +++ b/src-tauri/src/services/tdlib_v8/bootstrap.js @@ -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} 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} 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} 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} */ 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'); diff --git a/src-tauri/src/services/tdlib_v8/mod.rs b/src-tauri/src/services/tdlib_v8/mod.rs index 8623c47e0..aa590cdfb 100644 --- a/src-tauri/src/services/tdlib_v8/mod.rs +++ b/src-tauri/src/services/tdlib_v8/mod.rs @@ -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; diff --git a/src-tauri/src/services/tdlib_v8/ops/mod.rs b/src-tauri/src/services/tdlib_v8/ops/mod.rs deleted file mode 100644 index fa274e813..000000000 --- a/src-tauri/src/services/tdlib_v8/ops/mod.rs +++ /dev/null @@ -1,1236 +0,0 @@ -//! Custom Deno Core Ops for V8 Runtime -//! -//! Provides browser API implementations as Rust ops. - -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -use deno_core::{extension, op2, OpState}; -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; - -use super::storage::IdbStorage; - -// ============================================================================ -// Extension Definition -// ============================================================================ - -extension!( - alphahuman_ops, - ops = [ - // Console ops - op_console_log, - op_console_warn, - op_console_error, - // Crypto ops - op_crypto_random, - op_atob, - op_btoa, - // Performance ops - op_performance_now, - // Platform ops - op_platform_os, - op_platform_env, - // Timer ops (prefixed to avoid conflict with deno_core built-ins) - op_ah_timer_start, - op_ah_timer_cancel, - // Fetch ops - op_fetch, - // WebSocket ops - op_ws_connect, - op_ws_send, - op_ws_recv, - op_ws_close, - // IndexedDB ops - op_idb_open, - op_idb_close, - op_idb_delete_database, - op_idb_create_object_store, - op_idb_delete_object_store, - op_idb_get, - op_idb_put, - op_idb_delete, - op_idb_clear, - op_idb_get_all, - op_idb_get_all_keys, - op_idb_count, - // Skill bridge ops - op_db_exec, - op_db_get, - op_db_all, - op_db_kv_get, - op_db_kv_set, - op_store_get, - op_store_set, - op_store_delete, - op_store_keys, - op_net_fetch, - // State bridge ops - op_state_get, - op_state_set, - op_state_set_partial, - // Data bridge ops - op_data_read, - op_data_write, - // TDLib ops (telegram skill only) - op_tdlib_create_client, - op_tdlib_send, - op_tdlib_receive, - op_tdlib_destroy, - op_tdlib_is_available, - // Model ops (local LLM inference) - op_model_is_available, - op_model_get_status, - op_model_generate, - op_model_summarize, - ], - state = |state| { - // State will be initialized when runtime is created - } -); - -/// Build the deno_core Extension with all custom ops. -pub fn build_extension(_storage: IdbStorage) -> deno_core::Extension { - alphahuman_ops::init_ops_and_esm() -} - -/// Initialize storage in op state with data directory and shared skill state. -pub fn init_state_with_data_dir( - state: &mut OpState, - storage: IdbStorage, - skill_id: String, - data_dir: std::path::PathBuf, - skill_state: std::sync::Arc>, -) { - state.put(storage); - state.put(SkillContext::with_state(skill_id, data_dir, skill_state)); - state.put(TimerState::default()); - state.put(WebSocketState::default()); -} - -/// Poll timers and return IDs of timers that are ready to fire. -/// Also returns the duration until the next timer (for efficient sleeping). -pub fn poll_timers(state: &mut OpState) -> (Vec, Option) { - let timer_state = state.borrow_mut::(); - let ready = timer_state.poll_ready(); - let next = timer_state.time_until_next(); - (ready, next) -} - -/// Context for the current skill execution. -#[derive(Clone)] -pub struct SkillContext { - pub skill_id: String, - /// Skill-specific data directory for file I/O. - pub data_dir: Option, - /// Shared reference to the skill's state (same instance as V8SkillInstance). - pub skill_state: Option>>, -} - -impl Default for SkillContext { - fn default() -> Self { - Self { - skill_id: String::new(), - data_dir: None, - skill_state: None, - } - } -} - -impl SkillContext { - /// Create a new SkillContext with a skill ID, data directory, and shared state. - pub fn with_state( - skill_id: String, - data_dir: std::path::PathBuf, - skill_state: std::sync::Arc>, - ) -> Self { - Self { - skill_id, - data_dir: Some(data_dir), - skill_state: Some(skill_state), - } - } -} - -// ============================================================================ -// Timer State -// ============================================================================ - -/// A scheduled timer (setTimeout or setInterval). -#[derive(Clone, Debug)] -pub struct TimerEntry { - /// Whether this is a repeating interval - pub is_interval: bool, - /// Delay in milliseconds - pub delay_ms: u64, - /// When the timer should fire next (Instant) - pub next_fire: std::time::Instant, -} - -/// State for managing timers. -/// Timers are polled during the event loop and fire callbacks via JS. -#[derive(Default)] -pub struct TimerState { - /// Active timers: id -> TimerEntry - pub timers: HashMap, -} - -impl TimerState { - /// Get all timers that are ready to fire, returning their IDs. - /// For intervals, reschedules the next fire time. - /// For timeouts, removes them from the map. - pub fn poll_ready(&mut self) -> Vec { - let now = std::time::Instant::now(); - let mut ready = Vec::new(); - - // Collect IDs of ready timers - let ready_ids: Vec = self - .timers - .iter() - .filter(|(_, entry)| now >= entry.next_fire) - .map(|(id, _)| *id) - .collect(); - - for id in ready_ids { - if let Some(entry) = self.timers.get_mut(&id) { - ready.push(id); - - if entry.is_interval { - // Reschedule for next interval - entry.next_fire = now + std::time::Duration::from_millis(entry.delay_ms); - } else { - // Remove one-shot timeout - self.timers.remove(&id); - } - } - } - - ready - } - - /// Get the duration until the next timer fires (for sleep optimization). - pub fn time_until_next(&self) -> Option { - let now = std::time::Instant::now(); - self.timers - .values() - .map(|entry| { - if entry.next_fire > now { - entry.next_fire - now - } else { - std::time::Duration::ZERO - } - }) - .min() - } -} - -// ============================================================================ -// WebSocket State -// ============================================================================ - -/// State for managing WebSocket connections. -/// Currently a placeholder - full state management is TODO. -#[derive(Default)] -#[allow(dead_code)] -pub struct WebSocketState { - next_id: u32, - /// Active connections: id -> WebSocket sender - pub connections: HashMap, -} - -#[allow(dead_code)] -pub struct WebSocketConnection { - pub write_tx: mpsc::Sender, - pub read_rx: mpsc::Receiver, - pub close_tx: Option>, -} - -// ============================================================================ -// Console Ops -// ============================================================================ - -#[op2(fast)] -fn op_console_log(#[string] msg: &str) { - log::info!("[js] {}", msg); -} - -#[op2(fast)] -fn op_console_warn(#[string] msg: &str) { - log::warn!("[js] {}", msg); -} - -#[op2(fast)] -fn op_console_error(#[string] msg: &str) { - log::error!("[js] {}", msg); -} - -// ============================================================================ -// Crypto Ops -// ============================================================================ - -#[op2] -#[buffer] -fn op_crypto_random(len: u32) -> Vec { - use rand::RngCore; - let mut bytes = vec![0u8; len as usize]; - rand::thread_rng().fill_bytes(&mut bytes); - bytes -} - -#[op2] -#[string] -fn op_atob(#[string] input: &str) -> Result { - use base64::Engine; - let decoded = base64::engine::general_purpose::STANDARD.decode(input)?; - Ok(String::from_utf8_lossy(&decoded).to_string()) -} - -#[op2] -#[string] -fn op_btoa(#[string] input: &str) -> String { - use base64::Engine; - base64::engine::general_purpose::STANDARD.encode(input.as_bytes()) -} - -// ============================================================================ -// Performance Ops -// ============================================================================ - -#[op2(fast)] -fn op_performance_now() -> f64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs_f64() * 1000.0) - .unwrap_or(0.0) -} - -// ============================================================================ -// Platform Ops -// ============================================================================ - -#[op2] -#[string] -fn op_platform_os() -> &'static str { - #[cfg(target_os = "windows")] - return "windows"; - #[cfg(target_os = "macos")] - return "macos"; - #[cfg(target_os = "linux")] - return "linux"; - #[cfg(target_os = "android")] - return "android"; - #[cfg(target_os = "ios")] - return "ios"; - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "android", - target_os = "ios" - )))] - return "unknown"; -} - -#[op2] -#[string] -fn op_platform_env(#[string] key: &str) -> Option { - const ALLOWED_ENV_VARS: &[&str] = &[ - "VITE_TELEGRAM_BOT_USERNAME", - "VITE_TELEGRAM_BOT_ID", - "TELEGRAM_API_ID", // Skills may use this without VITE_ prefix - "TELEGRAM_API_HASH", // Skills may use this without VITE_ prefix - "VITE_BACKEND_URL", - "BACKEND_URL", // Skills may use this without VITE_ prefix - "VITE_DEBUG", - ]; - - if ALLOWED_ENV_VARS.contains(&key) { - std::env::var(key).ok() - } else { - None - } -} - -// ============================================================================ -// Timer Ops -// ============================================================================ - -/// Start a timer (setTimeout or setInterval). -/// The actual callback execution happens in JavaScript via __handleTimer, -/// triggered by the event loop polling TimerState. -#[op2(fast)] -fn op_ah_timer_start(state: &mut OpState, id: u32, delay_ms: u32, is_interval: bool) { - let timer_state = state.borrow_mut::(); - - let entry = TimerEntry { - is_interval, - delay_ms: delay_ms as u64, - next_fire: std::time::Instant::now() + std::time::Duration::from_millis(delay_ms as u64), - }; - - timer_state.timers.insert(id, entry); - log::debug!( - "[timer] Registered {} {} with delay {}ms", - if is_interval { "interval" } else { "timeout" }, - id, - delay_ms - ); -} - -/// Cancel a timer. -#[op2(fast)] -fn op_ah_timer_cancel(state: &mut OpState, id: u32) { - let timer_state = state.borrow_mut::(); - - if timer_state.timers.remove(&id).is_some() { - log::debug!("[timer] Cancelled timer {}", id); - } -} - -// ============================================================================ -// Fetch Ops -// ============================================================================ - -#[derive(Deserialize)] -struct FetchOptions { - method: Option, - headers: Option>, - body: Option, -} - -#[derive(Serialize)] -struct FetchResponse { - status: u16, - #[serde(rename = "statusText")] - status_text: String, - headers: HashMap, - body: String, -} - -/// Async fetch operation for the fetch API. -#[op2(async)] -#[serde] -async fn op_fetch( - #[string] url: String, - #[serde] options: FetchOptions, -) -> Result { - let client = reqwest::Client::new(); - - let method = options.method.unwrap_or_else(|| "GET".to_string()); - let method: reqwest::Method = method.parse().map_err(|_| { - deno_core::error::generic_error(format!("Invalid HTTP method: {}", method)) - })?; - - let mut request = client.request(method, &url); - - // Add headers - if let Some(headers) = options.headers { - for (key, value) in headers { - request = request.header(&key, &value); - } - } - - // Add body - if let Some(body) = options.body { - request = request.body(body); - } - - let response = request.send().await.map_err(|e| { - deno_core::error::generic_error(format!("Fetch failed: {}", e)) - })?; - - let status = response.status().as_u16(); - let status_text = response.status().canonical_reason().unwrap_or("").to_string(); - - let mut headers = HashMap::new(); - for (key, value) in response.headers() { - if let Ok(v) = value.to_str() { - headers.insert(key.to_string(), v.to_string()); - } - } - - let body = response.text().await.map_err(|e| { - deno_core::error::generic_error(format!("Failed to read response body: {}", e)) - })?; - - Ok(FetchResponse { - status, - status_text, - headers, - body, - }) -} - -// ============================================================================ -// WebSocket Ops -// ============================================================================ - -/// Connect to a WebSocket server. -#[op2(async)] -async fn op_ws_connect( - #[string] url: String, -) -> Result { - use futures::StreamExt; - use tokio_tungstenite::connect_async; - - let (ws_stream, _) = connect_async(&url).await.map_err(|e| { - deno_core::error::generic_error(format!("WebSocket connect failed: {}", e)) - })?; - - let (write, mut read) = ws_stream.split(); - - // Create channels for communication - // Note: These are set up for future full WebSocket state management - let (_write_tx, write_rx) = mpsc::channel::(32); - let (read_tx, _read_rx) = mpsc::channel::(32); - let (_close_tx, close_rx) = tokio::sync::oneshot::channel::<()>(); - - // Spawn write task - let _write_handle = tokio::spawn(async move { - use futures::SinkExt; - use tokio_tungstenite::tungstenite::Message; - - let mut write = write; - let mut write_rx_inner = write_rx; - let mut close_rx = close_rx; - - loop { - tokio::select! { - msg = write_rx_inner.recv() => { - match msg { - Some(text) => { - if write.send(Message::Text(text)).await.is_err() { - break; - } - } - None => break, - } - } - _ = &mut close_rx => { - let _ = write.close().await; - break; - } - } - } - }); - - // Spawn read task - let _read_handle = tokio::spawn(async move { - use tokio_tungstenite::tungstenite::Message; - - while let Some(result) = read.next().await { - match result { - Ok(Message::Text(text)) => { - if read_tx.send(text).await.is_err() { - break; - } - } - Ok(Message::Binary(data)) => { - // Convert binary to base64 for JavaScript - use base64::Engine; - let b64 = base64::engine::general_purpose::STANDARD.encode(&data); - if read_tx.send(b64).await.is_err() { - break; - } - } - Ok(Message::Close(_)) => break, - Err(_) => break, - _ => {} // Ignore ping/pong - } - } - }); - - // Generate a simple ID (in production, use proper state management) - let id = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u32) - .unwrap_or(1); - - log::info!("[ws] Connected to {} with id {}", url, id); - - Ok(id) -} - -/// Send a message over WebSocket. -#[op2(fast)] -fn op_ws_send( - _state: &mut OpState, - _id: u32, - #[string] _data: &str, -) -> Result<(), deno_core::error::AnyError> { - // Note: Full WebSocket state management would require more complex handling - // For now, this is a placeholder - log::debug!("[ws] Send message on connection {}", _id); - Ok(()) -} - -/// Receive a message from WebSocket (async). -#[op2(async)] -#[string] -async fn op_ws_recv(_id: u32) -> Result, deno_core::error::AnyError> { - // Note: Full WebSocket state management would require more complex handling - // For now, return None to indicate no message - Ok(None) -} - -/// Close a WebSocket connection. -#[op2(fast)] -fn op_ws_close( - _state: &mut OpState, - _id: u32, - _code: u16, - #[string] _reason: &str, -) -> Result<(), deno_core::error::AnyError> { - log::debug!("[ws] Close connection {}", _id); - Ok(()) -} - -// ============================================================================ -// IndexedDB Ops -// ============================================================================ - -#[derive(Serialize)] -struct IdbOpenResult { - #[serde(rename = "needsUpgrade")] - needs_upgrade: bool, - #[serde(rename = "oldVersion")] - old_version: u32, - #[serde(rename = "objectStores")] - object_stores: Vec, -} - -/// Open an IndexedDB database. -#[op2(async)] -#[serde] -async fn op_idb_open( - state: Rc>, - #[string] name: String, - version: u32, -) -> Result { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - let result = storage.open_database(&name, version).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB open failed: {}", e)) - })?; - - Ok(IdbOpenResult { - needs_upgrade: result.needs_upgrade, - old_version: result.old_version, - object_stores: result.object_stores, - }) -} - -/// Close an IndexedDB database. -#[op2(fast)] -fn op_idb_close(state: &mut OpState, #[string] name: &str) { - let storage = state.borrow::(); - storage.close_database(name); -} - -/// Delete an IndexedDB database. -#[op2(async)] -async fn op_idb_delete_database( - state: Rc>, - #[string] name: String, -) -> Result<(), deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.delete_database(&name).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB delete database failed: {}", e)) - }) -} - -#[derive(Deserialize)] -struct CreateObjectStoreOptions { - #[serde(rename = "keyPath")] - key_path: Option, - #[serde(rename = "autoIncrement")] - auto_increment: Option, -} - -/// Create an object store. -#[op2] -fn op_idb_create_object_store( - state: &mut OpState, - #[string] db_name: &str, - #[string] store_name: &str, - #[serde] options: CreateObjectStoreOptions, -) -> Result<(), deno_core::error::AnyError> { - let storage = state.borrow::(); - - storage - .create_object_store( - db_name, - store_name, - options.key_path.as_deref(), - options.auto_increment.unwrap_or(false), - ) - .map_err(|e| deno_core::error::generic_error(format!("Create object store failed: {}", e))) -} - -/// Delete an object store. -#[op2(fast)] -fn op_idb_delete_object_store( - state: &mut OpState, - #[string] db_name: &str, - #[string] store_name: &str, -) -> Result<(), deno_core::error::AnyError> { - let storage = state.borrow::(); - - storage - .delete_object_store(db_name, store_name) - .map_err(|e| deno_core::error::generic_error(format!("Delete object store failed: {}", e))) -} - -/// Get a value from an object store. -#[op2(async)] -#[serde] -async fn op_idb_get( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, - #[serde] key: serde_json::Value, -) -> Result, deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.get(&db_name, &store_name, &key).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB get failed: {}", e)) - }) -} - -/// Put a value into an object store. -#[op2(async)] -async fn op_idb_put( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, - #[serde] key: serde_json::Value, - #[serde] value: serde_json::Value, -) -> Result<(), deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.put(&db_name, &store_name, &key, &value).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB put failed: {}", e)) - }) -} - -/// Delete a value from an object store. -#[op2(async)] -async fn op_idb_delete( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, - #[serde] key: serde_json::Value, -) -> Result<(), deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.delete(&db_name, &store_name, &key).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB delete failed: {}", e)) - }) -} - -/// Clear all values from an object store. -#[op2(async)] -async fn op_idb_clear( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, -) -> Result<(), deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.clear(&db_name, &store_name).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB clear failed: {}", e)) - }) -} - -/// Get all values from an object store. -#[op2(async)] -#[serde] -async fn op_idb_get_all( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, - count: Option, -) -> Result, deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.get_all(&db_name, &store_name, count).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB get_all failed: {}", e)) - }) -} - -/// Get all keys from an object store. -#[op2(async)] -#[serde] -async fn op_idb_get_all_keys( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, - count: Option, -) -> Result, deno_core::error::AnyError> { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.get_all_keys(&db_name, &store_name, count).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB get_all_keys failed: {}", e)) - }) -} - -/// Count values in an object store. -#[op2(async)] -async fn op_idb_count( - state: Rc>, - #[string] db_name: String, - #[string] store_name: String, -) -> Result { - let storage = { - let state = state.borrow(); - state.borrow::().clone() - }; - - storage.count(&db_name, &store_name).await.map_err(|e| { - deno_core::error::generic_error(format!("IDB count failed: {}", e)) - }) -} - -// ============================================================================ -// Skill Bridge Ops (db, store, net) -// ============================================================================ - -#[op2] -#[bigint] -fn op_db_exec( - state: &mut OpState, - #[string] sql: &str, - #[string] params_json: Option, -) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - let params: Vec = match params_json { - Some(p) => serde_json::from_str(&p).unwrap_or_default(), - None => Vec::new(), - }; - - storage - .skill_db_exec(&ctx.skill_id, sql, ¶ms) - .map(|n| n as i64) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_db_get( - state: &mut OpState, - #[string] sql: &str, - #[string] params_json: Option, -) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - let params: Vec = match params_json { - Some(p) => serde_json::from_str(&p).unwrap_or_default(), - None => Vec::new(), - }; - - storage - .skill_db_get(&ctx.skill_id, sql, ¶ms) - .map(|v| v.to_string()) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_db_all( - state: &mut OpState, - #[string] sql: &str, - #[string] params_json: Option, -) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - let params: Vec = match params_json { - Some(p) => serde_json::from_str(&p).unwrap_or_default(), - None => Vec::new(), - }; - - storage - .skill_db_all(&ctx.skill_id, sql, ¶ms) - .map(|v| v.to_string()) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_db_kv_get(state: &mut OpState, #[string] key: &str) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - storage - .skill_kv_get(&ctx.skill_id, key) - .map(|v| v.to_string()) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2(fast)] -fn op_db_kv_set( - state: &mut OpState, - #[string] key: &str, - #[string] value_json: &str, -) -> Result<(), deno_core::error::AnyError> { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - let value: serde_json::Value = - serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null); - - storage - .skill_kv_set(&ctx.skill_id, key, &value) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_store_get(state: &mut OpState, #[string] key: &str) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - storage - .skill_store_get(&ctx.skill_id, key) - .map(|v| v.to_string()) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2(fast)] -fn op_store_set( - state: &mut OpState, - #[string] key: &str, - #[string] value_json: &str, -) -> Result<(), deno_core::error::AnyError> { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - let value: serde_json::Value = - serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null); - - storage - .skill_store_set(&ctx.skill_id, key, &value) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2(fast)] -fn op_store_delete(state: &mut OpState, #[string] key: &str) -> Result<(), deno_core::error::AnyError> { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - storage - .skill_store_delete(&ctx.skill_id, key) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_store_keys(state: &mut OpState) -> Result { - let storage = state.borrow::(); - let ctx = state.borrow::(); - - storage - .skill_store_keys(&ctx.skill_id) - .map(|keys| serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string())) - .map_err(|e| deno_core::error::generic_error(e)) -} - -#[op2] -#[string] -fn op_net_fetch( - #[string] url: &str, - #[string] options_json: &str, -) -> Result { - crate::runtime::bridge::net::http_fetch(url, options_json) - .map_err(|e| deno_core::error::generic_error(e)) -} - -// ============================================================================ -// State Bridge Ops -// ============================================================================ - -/// Get a value from the skill's published state. -#[op2] -#[string] -fn op_state_get(state: &mut OpState, #[string] key: &str) -> Result { - let ctx = state.borrow::(); - - let skill_state = ctx.skill_state.as_ref().ok_or_else(|| { - deno_core::error::generic_error("Skill state not initialized") - })?; - - let published_state = &skill_state.read().published_state; - let value = published_state.get(key).cloned().unwrap_or(serde_json::Value::Null); - Ok(value.to_string()) -} - -/// Set a value in the skill's published state. -#[op2(fast)] -fn op_state_set( - state: &mut OpState, - #[string] key: &str, - #[string] value_json: &str, -) -> Result<(), deno_core::error::AnyError> { - let ctx = state.borrow::(); - - let skill_state = ctx.skill_state.as_ref().ok_or_else(|| { - deno_core::error::generic_error("Skill state not initialized") - })?; - - let value: serde_json::Value = - serde_json::from_str(value_json).unwrap_or(serde_json::Value::Null); - skill_state.write().published_state.insert(key.to_string(), value); - Ok(()) -} - -/// Merge a partial object into the skill's published state. -#[op2(fast)] -fn op_state_set_partial( - state: &mut OpState, - #[string] partial_json: &str, -) -> Result<(), deno_core::error::AnyError> { - let ctx = state.borrow::(); - - let skill_state = ctx.skill_state.as_ref().ok_or_else(|| { - deno_core::error::generic_error("Skill state not initialized") - })?; - - let partial: serde_json::Value = - serde_json::from_str(partial_json).unwrap_or(serde_json::Value::Object(Default::default())); - - if let serde_json::Value::Object(map) = partial { - let mut state_guard = skill_state.write(); - for (k, v) in map { - state_guard.published_state.insert(k, v); - } - } - Ok(()) -} - -// ============================================================================ -// Data Bridge Ops -// ============================================================================ - -/// Read a file from the skill's data directory. -#[op2] -#[string] -fn op_data_read( - state: &mut OpState, - #[string] filename: &str, -) -> Result { - let ctx = state.borrow::(); - - let data_dir = ctx.data_dir.as_ref().ok_or_else(|| { - deno_core::error::generic_error("Data directory not configured") - })?; - - let path = data_dir.join(filename); - - // Prevent path traversal - if !path.starts_with(data_dir) { - return Err(deno_core::error::generic_error("Invalid filename: path traversal")); - } - - std::fs::read_to_string(&path).map_err(|e| { - deno_core::error::generic_error(format!("Failed to read file '{}': {}", filename, e)) - }) -} - -/// Write a file to the skill's data directory. -#[op2(fast)] -fn op_data_write( - state: &mut OpState, - #[string] filename: &str, - #[string] content: &str, -) -> Result<(), deno_core::error::AnyError> { - let ctx = state.borrow::(); - - let data_dir = ctx.data_dir.as_ref().ok_or_else(|| { - deno_core::error::generic_error("Data directory not configured") - })?; - - let path = data_dir.join(filename); - - // Prevent path traversal - if !path.starts_with(data_dir) { - return Err(deno_core::error::generic_error("Invalid filename: path traversal")); - } - - // Ensure parent directories exist - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| { - deno_core::error::generic_error(format!("Failed to create directory: {}", e)) - })?; - } - - std::fs::write(&path, content).map_err(|e| { - deno_core::error::generic_error(format!("Failed to write file '{}': {}", filename, e)) - }) -} - -// ============================================================================ -// TDLib Ops (telegram skill only) -// ============================================================================ - -/// Check if the current skill is the telegram skill. -fn check_telegram_skill(state: &OpState) -> Result<(), deno_core::error::AnyError> { - let ctx = state.borrow::(); - if ctx.skill_id != "telegram" { - return Err(deno_core::error::generic_error( - "TDLib is only available to the telegram skill", - )); - } - Ok(()) -} - -/// Check if TDLib is available (always true on desktop). -#[op2(fast)] -fn op_tdlib_is_available() -> bool { - true -} - -/// Create a TDLib client with the given data directory. -/// This is synchronous since create_client just spawns a worker thread and returns. -#[op2(fast)] -fn op_tdlib_create_client( - state: &mut OpState, - #[string] data_dir: String, -) -> Result { - // Check skill permission - check_telegram_skill(state)?; - - let path = std::path::PathBuf::from(data_dir); - - crate::services::tdlib::TDLIB_MANAGER - .create_client(path) - .map_err(|e| deno_core::error::generic_error(e)) -} - -/// Send a request to TDLib and wait for the response. -#[op2(async)] -#[serde] -async fn op_tdlib_send( - state: Rc>, - #[serde] request: serde_json::Value, -) -> Result { - // Check skill permission - { - let state = state.borrow(); - check_telegram_skill(&state)?; - } - - crate::services::tdlib::TDLIB_MANAGER - .send(request) - .await - .map_err(|e| deno_core::error::generic_error(e)) -} - -/// Receive the next update from TDLib (with timeout in ms). -#[op2(async)] -#[serde] -async fn op_tdlib_receive( - state: Rc>, - timeout_ms: u32, -) -> Result, deno_core::error::AnyError> { - // Check skill permission - { - let state = state.borrow(); - check_telegram_skill(&state)?; - } - - Ok(crate::services::tdlib::TDLIB_MANAGER.receive(timeout_ms).await) -} - -/// Destroy the TDLib client and clean up resources. -#[op2(async)] -async fn op_tdlib_destroy( - state: Rc>, -) -> Result<(), deno_core::error::AnyError> { - // Check skill permission - { - let state = state.borrow(); - check_telegram_skill(&state)?; - } - - crate::services::tdlib::TDLIB_MANAGER - .destroy() - .await - .map_err(|e| deno_core::error::generic_error(e)) -} - -// ============================================================================ -// Model Ops (local LLM inference) -// ============================================================================ - -/// Check if local model API is available (desktop only). -#[op2(fast)] -fn op_model_is_available() -> bool { - true -} - -/// Get model status (loading, ready, error). -#[op2] -#[serde] -fn op_model_get_status() -> serde_json::Value { - let status = crate::services::llama::LLAMA_MANAGER.get_status(); - serde_json::to_value(status).unwrap_or_default() -} - -/// Generate text from prompt (async, blocking inference on thread pool). -#[op2(async)] -#[string] -async fn op_model_generate( - #[string] prompt: String, - #[serde] config: serde_json::Value, -) -> Result { - let cfg: crate::services::llama::GenerateConfig = - serde_json::from_value(config).unwrap_or_default(); - - crate::services::llama::LLAMA_MANAGER - .generate(&prompt, cfg) - .await - .map_err(|e| deno_core::error::generic_error(e)) -} - -/// Summarize text (async). -#[op2(async)] -#[string] -async fn op_model_summarize( - #[string] text: String, - max_tokens: u32, -) -> Result { - crate::services::llama::LLAMA_MANAGER - .summarize(&text, max_tokens) - .await - .map_err(|e| deno_core::error::generic_error(e)) -} diff --git a/src-tauri/src/services/tdlib_v8/qjs_ops.rs b/src-tauri/src/services/tdlib_v8/qjs_ops.rs new file mode 100644 index 000000000..95003d42b --- /dev/null +++ b/src-tauri/src/services/tdlib_v8/qjs_ops.rs @@ -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, +} + +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, +} + +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, + 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>, + timer_state: Arc>, + ws_state: Arc>, +) -> 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 { + 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| 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 { + 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 = 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::(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.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 { + 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.clone(); + let sc = skill_context.clone(); + 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())) + }, + ))?; + } + + // ======================================================================== + // Net (1) + // ======================================================================== + + ops.set("net_fetch", Function::new(ctx.clone(), + |url: String, options_json: String| -> rquickjs::Result { + 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 { + 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 = + 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 { + 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 { + 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::, 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 { + 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(()) +} diff --git a/src-tauri/src/services/tdlib_v8/storage.rs b/src-tauri/src/services/tdlib_v8/storage.rs index 4f6b27a21..955c9ffb0 100644 --- a/src-tauri/src/services/tdlib_v8/storage.rs +++ b/src-tauri/src/services/tdlib_v8/storage.rs @@ -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>>>>, + connections: Arc>>>>, /// Database version info (used by IndexedDB emulation). #[allow(dead_code)] versions: Arc>>, @@ -49,7 +49,7 @@ impl IdbStorage { } /// Get or create a database connection. - fn get_connection(&self, db_name: &str) -> Result>, String> { + 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()); @@ -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 { + pub fn open_database(&self, name: &str, version: u32) -> Result { 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, 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, ) -> 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 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, ) -> 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 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 { + pub fn count(&self, db_name: &str, store_name: &str) -> Result { 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 { 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> = params .iter() @@ -418,7 +418,7 @@ impl IdbStorage { ) -> Result { 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> = params .iter() @@ -463,7 +463,7 @@ impl IdbStorage { ) -> Result { 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> = params .iter() @@ -502,7 +502,7 @@ impl IdbStorage { 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.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, 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 diff --git a/src-tauri/src/utils/config.rs b/src-tauri/src/utils/config.rs index 4f5823a81..db6f20c3b 100644 --- a/src-tauri/src/utils/config.rs +++ b/src-tauri/src/utils/config.rs @@ -3,8 +3,6 @@ /// This module provides configuration values that can be /// overridden via environment variables at runtime. use std::env; -use std::path::PathBuf; -use std::sync::Once; /// Default backend URL (can be overridden via BACKEND_URL env var) pub const DEFAULT_BACKEND_URL: &str = "https://api.alphahuman.xyz"; @@ -15,55 +13,9 @@ pub const APP_IDENTIFIER: &str = "com.alphahuman.app"; /// Service name for keychain pub const KEYCHAIN_SERVICE: &str = "AlphaHuman"; -/// Ensure .env is loaded once -static DOTENV_INIT: Once = Once::new(); - -/// Try to find and load .env file from various locations -fn ensure_dotenv_loaded() { - DOTENV_INIT.call_once(|| { - // Try current directory first (project root when running `tauri dev`) - if dotenvy::dotenv().is_ok() { - log::debug!("[config] Loaded .env from current directory"); - return; - } - - // Try parent directory (when cwd is src-tauri) - if let Ok(cwd) = env::current_dir() { - let parent_env = cwd.parent().map(|p| p.join(".env")); - if let Some(path) = parent_env { - if path.exists() { - if dotenvy::from_path(&path).is_ok() { - log::debug!("[config] Loaded .env from parent directory: {:?}", path); - return; - } - } - } - } - - // Try CARGO_MANIFEST_DIR (available during development builds) - if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { - let manifest_path = PathBuf::from(&manifest_dir); - // .env is one level up from src-tauri - let project_root_env = manifest_path.parent().map(|p| p.join(".env")); - if let Some(path) = project_root_env { - if path.exists() { - if dotenvy::from_path(&path).is_ok() { - log::debug!("[config] Loaded .env from project root: {:?}", path); - return; - } - } - } - } - - log::debug!("[config] No .env file found, using defaults/environment"); - }); -} - /// Get the backend URL from environment or use default /// Checks VITE_BACKEND_URL first, then BACKEND_URL, then defaults pub fn get_backend_url() -> String { - ensure_dotenv_loaded(); - let url = env::var("VITE_BACKEND_URL") .or_else(|_| env::var("BACKEND_URL")) .unwrap_or_else(|_| DEFAULT_BACKEND_URL.to_string()); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a596e65fc..848569802 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "AlphaHuman", - "version": "0.1.0", + "version": "0.31.0", "identifier": "com.alphahuman.app", "build": { "beforeDevCommand": "npm run dev",