diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index 2a43a412..a15ba2de 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "openless-app", - "version": "1.3.13-Beta.1", + "version": "1.3.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openless-app", - "version": "1.3.13-Beta.1", + "version": "1.3.14", "dependencies": { "@formkit/auto-animate": "^0.9.0", "@tauri-apps/api": "^2.1.1", @@ -29,17 +29,17 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "typescript": "^5.6.3", - "vite": "^5.4.10" + "vite": "^6.4.3" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -48,9 +48,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -58,21 +58,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -89,14 +89,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -106,14 +106,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -123,9 +123,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -133,29 +133,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -175,9 +175,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -185,9 +185,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -195,9 +195,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -205,27 +205,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -276,33 +276,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -310,23 +310,23 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -337,13 +337,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -354,13 +354,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -371,13 +371,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -388,13 +388,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -405,13 +405,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -422,13 +422,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -439,13 +439,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -456,13 +456,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -473,13 +473,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -490,13 +490,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -507,13 +507,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -524,13 +524,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -541,13 +541,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -558,13 +558,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -575,13 +575,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -592,13 +592,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -609,13 +609,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -626,13 +643,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -643,13 +677,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -660,13 +711,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -677,13 +728,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -694,13 +745,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -711,7 +762,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@formkit/auto-animate": { @@ -1507,9 +1558,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.24", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", - "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1520,9 +1571,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -1540,10 +1591,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -1554,9 +1605,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -1607,25 +1658,25 @@ } }, "node_modules/dompurify": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", - "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "version": "1.5.380", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", + "integrity": "sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==", "dev": true, "license": "ISC" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1633,32 +1684,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -1671,6 +1725,24 @@ "node": ">=6" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/framer-motion": { "version": "12.39.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.39.0.tgz", @@ -1868,11 +1940,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -1881,6 +1956,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", @@ -2046,6 +2134,23 @@ "node": ">=0.10.0" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2107,21 +2212,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2130,19 +2238,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2163,6 +2277,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, diff --git a/openless-all/app/package.json b/openless-all/app/package.json index 45d7fdd1..ab314f71 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -1,7 +1,7 @@ { "name": "openless-app", "private": true, - "version": "1.3.13-Beta.1", + "version": "1.3.14", "type": "module", "scripts": { "dev": "vite", @@ -44,6 +44,6 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "typescript": "^5.6.3", - "vite": "^5.4.10" + "vite": "^6.4.3" } } diff --git a/openless-all/app/src-tauri/Cargo.lock b/openless-all/app/src-tauri/Cargo.lock index bae2f703..e621b2ec 100644 --- a/openless-all/app/src-tauri/Cargo.lock +++ b/openless-all/app/src-tauri/Cargo.lock @@ -3528,6 +3528,9 @@ name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" +dependencies = [ + "cc", +] [[package]] name = "objc2" @@ -3883,7 +3886,7 @@ dependencies = [ [[package]] name = "openless" -version = "1.3.13-Beta.1" +version = "1.3.14" dependencies = [ "anyhow", "arboard", diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index ff16aed3..aa103296 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openless" -version = "1.3.13-Beta.1" +version = "1.3.14" description = "OpenLess — local voice input that types where your cursor is" authors = ["OpenLess"] edition = "2021" @@ -108,7 +108,7 @@ core-graphics = "0.24" # (替代 10s 轮询,空闲零唤醒)。符号本就在依赖树(cpal → coreaudio-sys 0.2),提升为 # 直接依赖零额外编译成本。 coreaudio-sys = "0.2" -objc2 = "0.5" +objc2 = { version = "0.5", features = ["exception"] } objc2-foundation = "0.2" objc2-app-kit = "0.2" # qwen-asr 用 malloc 分配返回字符串,必须用 C `free()` 释放。 diff --git a/openless-all/app/src-tauri/src/asr/local/apple_speech_provider.rs b/openless-all/app/src-tauri/src/asr/local/apple_speech_provider.rs index 91628f68..1c558d67 100644 --- a/openless-all/app/src-tauri/src/asr/local/apple_speech_provider.rs +++ b/openless-all/app/src-tauri/src/asr/local/apple_speech_provider.rs @@ -39,16 +39,25 @@ const SF_AUTH_AUTHORIZED: i64 = 3; /// 这里只防 block 永不回调导致线程永久阻塞。 const RECOGNITION_WAIT: Duration = Duration::from_secs(60); const AUTHORIZATION_WAIT: Duration = Duration::from_secs(30); +/// 识别引擎就绪(isAvailable)的轮询等待:刚 init 的 recognizer 常瞬时不可用(异步 +/// 加载语言资源),稍等即就绪。等满仍不可用才报错——修「有时用不了」的竞态。 +const AVAILABILITY_WAIT: Duration = Duration::from_secs(3); +const AVAILABILITY_POLL: Duration = Duration::from_millis(100); pub struct AppleSpeechAsr { /// 16-bit LE PCM 字节缓冲(recorder 推什么我们存什么)。与 LocalQwenAsr 同形。 buffer: Mutex>, + /// 识别 locale(Apple 标识符,如 "zh-CN")。None = 用系统默认。由用户工作语言映射 + /// 而来 —— SFSpeechRecognizer 一个实例只认一种语言,不显式指定就落到系统首选语言 + /// (常是英文),中文语音会被英文引擎识别成英文且理解错误(用户报告的根因)。 + locale: Option, } impl AppleSpeechAsr { - pub fn new() -> Self { + pub fn new(locale: Option) -> Self { Self { buffer: Mutex::new(Vec::new()), + locale, } } @@ -73,12 +82,13 @@ impl AppleSpeechAsr { }); } let duration_ms = (pcm.len() as u64 / 2) * 1000 / 16_000; + let locale = self.locale.clone(); // SFSpeechRecognizer 是阻塞且基于 objc runloop 的同步桥接;放到 // spawn_blocking 不占 tokio runtime。与 LocalQwenAsr 走同一个 Tauri // 持有的 runtime handle。 let result = tauri::async_runtime::spawn_blocking(move || { - transcribe_pcm_blocking(&pcm, duration_ms) + transcribe_pcm_blocking(&pcm, duration_ms, locale.as_deref()) }) .await .context("apple-speech transcribe spawn_blocking join 失败")?; @@ -96,7 +106,7 @@ impl AppleSpeechAsr { impl Default for AppleSpeechAsr { fn default() -> Self { - Self::new() + Self::new(None) } } @@ -108,7 +118,11 @@ impl crate::recorder::AudioConsumer for AppleSpeechAsr { /// 把 PCM 写成临时 wav,确保授权,跑批处理识别,删临时文件,返回结果。 /// 在 spawn_blocking 线程内同步执行。 -fn transcribe_pcm_blocking(pcm: &[u8], duration_ms: u64) -> Result { +fn transcribe_pcm_blocking( + pcm: &[u8], + duration_ms: u64, + locale: Option<&str>, +) -> Result { ensure_authorized()?; let samples: Vec = pcm @@ -129,7 +143,7 @@ fn transcribe_pcm_blocking(pcm: &[u8], duration_ms: u64) -> Result Result<()> { /// 用 `SFSpeechURLRecognitionRequest` 对给定 wav 文件做一次批处理识别, /// 把 `recognitionTaskWithRequest:resultHandler:` 的异步回调同步化。 -fn recognize_file(wav_path: &str) -> Result { - let recognizer = create_recognizer()?; +fn recognize_file(wav_path: &str, locale: Option<&str>) -> Result { + let recognizer = create_recognizer(locale)?; - // recognizer.isAvailable —— 识别引擎当前是否可用(首次可能在下载语言资源)。 - // SAFETY: `recognizer` 是有效的 `SFSpeechRecognizer` 实例;`isAvailable` 无参,返回 BOOL。 - let available: Bool = unsafe { msg_send![recognizer, isAvailable] }; - if !available.as_bool() { - bail!("当前语言的语音识别暂不可用(系统可能正在准备识别资源,请稍后重试)"); - } + // 识别引擎就绪等待(isAvailable 竞态):SFSpeechRecognizer 刚 init 时引擎往往还没 + // 就绪(异步加载语言资源),isAvailable 瞬时为 false、稍等即 true。之前一见 false + // 就 bail —— 这正是「有时用不了」的主因。改为轮询等待最多几秒再判定。 + wait_until_available(recognizer)?; let url = file_url(wav_path)?; let request = create_url_request(url)?; + // on-device 优先:设备支持当前语言的设备端识别就强制 on-device —— 音频不出本机 + // (隐私)、离线可用、不受网络波动/限流影响(消除「有时连不上服务器」)。不支持的 + // 语言回退系统默认(可能走网络)以保底能用。 + configure_on_device(recognizer, request); + let (tx, rx) = mpsc::channel::(); // resultHandler: void(^)(SFSpeechRecognitionResult *result, NSError *error) let block = RcBlock::new(move |result: *mut AnyObject, error: *mut AnyObject| { @@ -226,6 +243,44 @@ fn recognize_file(wav_path: &str) -> Result { } } +/// 轮询等待识别引擎就绪。init 后 isAvailable 可能瞬时 false(异步加载资源),稍等 +/// 即 true;等满 AVAILABILITY_WAIT 仍不可用才报错并引导。 +fn wait_until_available(recognizer: *mut AnyObject) -> Result<()> { + let deadline = std::time::Instant::now() + AVAILABILITY_WAIT; + loop { + // SAFETY: `recognizer` 有效;`isAvailable` 无参返回 BOOL。 + let available: Bool = unsafe { msg_send![recognizer, isAvailable] }; + if available.as_bool() { + return Ok(()); + } + if std::time::Instant::now() >= deadline { + bail!( + "当前语言的语音识别暂不可用:系统可能仍在准备识别资源,或需在 系统设置 → 键盘 → 听写 中下载对应语言。可稍后重试,或改用其它 ASR。" + ); + } + std::thread::sleep(AVAILABILITY_POLL); + } +} + +/// 支持设备端识别的语言就把请求设成强制 on-device(音频不出本机、离线可用);不支持 +/// 的语言不设,回退系统默认(可能走网络)以保底能用。 +fn configure_on_device(recognizer: *mut AnyObject, request: *mut AnyObject) { + // SFSpeechRecognizer.supportsOnDeviceRecognition(macOS 10.15+,BOOL 属性)。 + // SAFETY: `recognizer` 有效;无参返回 BOOL。 + let supports: Bool = unsafe { msg_send![recognizer, supportsOnDeviceRecognition] }; + if supports.as_bool() { + // SFSpeechRecognitionRequest.requiresOnDeviceRecognition = YES。 + // SAFETY: `request` 是 SFSpeechURLRecognitionRequest(父类 + // SFSpeechRecognitionRequest 提供该 setter);参数 BOOL。 + let _: () = unsafe { msg_send![request, setRequiresOnDeviceRecognition: Bool::new(true)] }; + log::info!("[apple-speech] on-device recognition enabled"); + } else { + log::info!( + "[apple-speech] on-device unsupported for current locale; using default (may use network)" + ); + } +} + /// 识别回调的归一化结果。 enum RecognitionOutcome { /// 还没拿到 final(partial),不发送。 @@ -271,21 +326,74 @@ fn speech_recognizer_class() -> Result<&'static AnyClass> { }) } -/// `[[SFSpeechRecognizer alloc] init]` —— 用系统当前 locale。 -fn create_recognizer() -> Result<*mut AnyObject> { +/// 创建 recognizer。有指定 locale 就 `initWithLocale:`(关键 —— 否则落到系统首选语言, +/// 中文语音会被英文引擎误识别);无 locale 或 NSLocale 构造失败时回退 `init`(系统默认)。 +fn create_recognizer(locale: Option<&str>) -> Result<*mut AnyObject> { let cls = speech_recognizer_class()?; - // SAFETY: `cls` 是 `SFSpeechRecognizer` 类;`alloc` 返回未初始化实例, - // `init` 对其初始化,返回的实例由本函数所有权移交调用方(随后被 ARC 管理)。 - let recognizer: *mut AnyObject = unsafe { - let alloc: *mut AnyObject = msg_send![cls, alloc]; - msg_send![alloc, init] + let recognizer: *mut AnyObject = match locale.and_then(ns_locale) { + Some(ns_loc) => { + log::info!("[apple-speech] recognizer locale = {}", locale.unwrap_or("")); + // SAFETY: `cls` 是 SFSpeechRecognizer 类;`alloc` 得未初始化实例, + // `initWithLocale:` 用有效 NSLocale 初始化,返回实例移交调用方(ARC 管理)。 + unsafe { + let alloc: *mut AnyObject = msg_send![cls, alloc]; + msg_send![alloc, initWithLocale: ns_loc] + } + } + None => { + // SAFETY: 同上;`init` 用系统默认 locale。 + unsafe { + let alloc: *mut AnyObject = msg_send![cls, alloc]; + msg_send![alloc, init] + } + } }; if recognizer.is_null() { - bail!("无法创建 SFSpeechRecognizer(当前系统语言可能不支持语音识别)"); + bail!("无法创建 SFSpeechRecognizer(当前语言可能不支持语音识别)"); } Ok(recognizer) } +/// `[NSLocale localeWithLocaleIdentifier:]`。构造失败返回 None(调用方回退系统默认)。 +fn ns_locale(identifier: &str) -> Option<*mut AnyObject> { + let ns_id = ns_string_from_str(identifier).ok()?; + let cls = AnyClass::get("NSLocale")?; + // SAFETY: `cls` 是 NSLocale;`localeWithLocaleIdentifier:` 接收 NSString(`ns_id` 有效), + // 返回 autoreleased NSLocale(在 spawn_blocking 线程的 autorelease 池存活)。 + let loc: *mut AnyObject = unsafe { msg_send![cls, localeWithLocaleIdentifier: ns_id] }; + if loc.is_null() { + None + } else { + Some(loc) + } +} + +/// 用户工作语言(原生名,见前端 `SUPPORTED_LANGUAGES`)→ SFSpeechRecognizer 的 locale +/// 标识符。取 `working_languages` 主语言映射;未收录的语言返回 None(回退系统默认 locale)。 +/// SFSpeechRecognizer 一个实例只认一种语言,中英混说时以主语言为准 —— 这是 Apple 的固有 +/// 限制,云端 ASR 才能自由多语言混识。 +pub fn native_name_to_apple_locale(native_name: &str) -> Option { + let locale = match native_name.trim() { + "简体中文" => "zh-CN", + "繁体中文" | "繁體中文" => "zh-TW", + "English" => "en-US", + "日本語" => "ja-JP", + "한국어" => "ko-KR", + "Français" => "fr-FR", + "Deutsch" => "de-DE", + "Español" => "es-ES", + "Italiano" => "it-IT", + "Português" => "pt-BR", + "Русский" => "ru-RU", + "العربية" => "ar-SA", + "Tiếng Việt" => "vi-VN", + "ไทย" => "th-TH", + "हिन्दी" => "hi-IN", + _ => return None, + }; + Some(locale.to_string()) +} + /// `[NSURL fileURLWithPath:]`。 fn file_url(path: &str) -> Result<*mut AnyObject> { let ns_path = ns_string_from_str(path)?; @@ -387,7 +495,7 @@ mod tests { #[test] fn buffer_duration_tracks_consumed_pcm() { - let asr = AppleSpeechAsr::new(); + let asr = AppleSpeechAsr::new(None); assert_eq!(asr.buffer_duration_ms(), 0); // 16k * 2 bytes/sample * 1s = 32000 bytes。 asr.consume_pcm_chunk(&vec![0u8; 32_000]); @@ -398,7 +506,7 @@ mod tests { #[test] fn cancel_clears_buffer() { - let asr = AppleSpeechAsr::new(); + let asr = AppleSpeechAsr::new(None); asr.consume_pcm_chunk(&vec![0u8; 32_000]); asr.cancel(); assert_eq!(asr.buffer_duration_ms(), 0); @@ -406,7 +514,7 @@ mod tests { #[tokio::test] async fn transcribe_empty_buffer_returns_empty() { - let asr = AppleSpeechAsr::new(); + let asr = AppleSpeechAsr::new(None); let transcript = asr.transcribe().await.unwrap(); assert_eq!(transcript.text, ""); assert_eq!(transcript.duration_ms, 0); diff --git a/openless-all/app/src-tauri/src/asr/local/mod.rs b/openless-all/app/src-tauri/src/asr/local/mod.rs index 627f3ecf..cb60aec9 100644 --- a/openless-all/app/src-tauri/src/asr/local/mod.rs +++ b/openless-all/app/src-tauri/src/asr/local/mod.rs @@ -38,7 +38,7 @@ mod qwen_ffi; #[cfg(target_os = "macos")] #[allow(unused_imports)] -pub use apple_speech_provider::AppleSpeechAsr; +pub use apple_speech_provider::{native_name_to_apple_locale, AppleSpeechAsr}; #[cfg(target_os = "macos")] pub use local_provider::LocalQwenAsr; #[cfg(target_os = "macos")] diff --git a/openless-all/app/src-tauri/src/asr/volcengine.rs b/openless-all/app/src-tauri/src/asr/volcengine.rs index f356710c..cc595600 100644 --- a/openless-all/app/src-tauri/src/asr/volcengine.rs +++ b/openless-all/app/src-tauri/src/asr/volcengine.rs @@ -32,6 +32,15 @@ const BYTES_PER_MS: f64 = 32.0; const HOTWORD_CAP: usize = 80; const FINAL_RESULT_TIMEOUT: Duration = Duration::from_secs(12); +/// 弱网下 TLS/WebSocket 握手可能一直挂到 OS 级 TCP 超时(几十秒),期间用户卡在 +/// 「Starting」无法语音输入。协调器的全局超时只覆盖 `await_final_result`,**不**覆盖 +/// `open_session`,所以这里必须自己给握手设上限:超时即快速失败并重试,而不是冻结。 +const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +/// 单次网络抖动(连接被重置 / 瞬时 DNS 失败)以前会直接让整次听写失败。重试几次让 +/// 抖动可恢复。`AuthRejected`(凭据被拒)不在重试之列——重试也不会变好,只会拖慢报错。 +const CONNECT_MAX_ATTEMPTS: usize = 3; +const CONNECT_RETRY_BACKOFF: Duration = Duration::from_millis(250); + #[derive(Clone, Debug)] pub struct VolcengineCredentials { pub app_id: String, @@ -131,34 +140,7 @@ impl VolcengineStreamingASR { } let connect_id = Uuid::new_v4().to_string(); - let mut request = ENDPOINT - .into_client_request() - .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?; - let headers = request.headers_mut(); - headers.insert( - "X-Api-App-Key", - HeaderValue::from_str(&self.credentials.app_id) - .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, - ); - headers.insert( - "X-Api-Access-Key", - HeaderValue::from_str(&self.credentials.access_token) - .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, - ); - headers.insert( - "X-Api-Resource-Id", - HeaderValue::from_str(&self.credentials.resource_id) - .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, - ); - headers.insert( - "X-Api-Connect-Id", - HeaderValue::from_str(&connect_id) - .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, - ); - - let (ws, _resp) = connect_async(request) - .await - .map_err(classify_connect_error)?; + let ws = self.connect_with_retry(&connect_id).await?; let (write, read) = ws.split(); let (tx, rx) = oneshot::channel(); @@ -260,6 +242,79 @@ impl VolcengineStreamingASR { Ok(()) } + /// Build the WebSocket handshake request (endpoint + auth headers). Rebuilt + /// per connect attempt because `connect_async` consumes the request and + /// `http::Request` is not `Clone`. + fn build_connect_request( + &self, + connect_id: &str, + ) -> Result + { + let mut request = ENDPOINT + .into_client_request() + .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?; + let headers = request.headers_mut(); + headers.insert( + "X-Api-App-Key", + HeaderValue::from_str(&self.credentials.app_id) + .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, + ); + headers.insert( + "X-Api-Access-Key", + HeaderValue::from_str(&self.credentials.access_token) + .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, + ); + headers.insert( + "X-Api-Resource-Id", + HeaderValue::from_str(&self.credentials.resource_id) + .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, + ); + headers.insert( + "X-Api-Connect-Id", + HeaderValue::from_str(connect_id) + .map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?, + ); + Ok(request) + } + + /// Connect with a per-attempt timeout and bounded retries so a poor network + /// (hung handshake or a transient blip) doesn't kill the whole dictation. + /// `AuthRejected` short-circuits — bad credentials never heal on retry. + async fn connect_with_retry(&self, connect_id: &str) -> Result { + let mut attempt = 0usize; + loop { + attempt += 1; + let request = self.build_connect_request(connect_id)?; + match tokio::time::timeout(CONNECT_TIMEOUT, connect_async(request)).await { + Ok(Ok((ws, _resp))) => return Ok(ws), + Ok(Err(e)) => { + let classified = classify_connect_error(e); + if matches!(classified, VolcengineASRError::AuthRejected(_)) + || attempt >= CONNECT_MAX_ATTEMPTS + { + return Err(classified); + } + log::warn!( + "[asr] 连接尝试 {attempt}/{CONNECT_MAX_ATTEMPTS} 失败: {classified};重试中" + ); + } + Err(_) => { + if attempt >= CONNECT_MAX_ATTEMPTS { + return Err(VolcengineASRError::ConnectionFailed(format!( + "连接超时({} ms)", + CONNECT_TIMEOUT.as_millis() + ))); + } + log::warn!( + "[asr] 连接尝试 {attempt}/{CONNECT_MAX_ATTEMPTS} 超时({} ms);重试中", + CONNECT_TIMEOUT.as_millis() + ); + } + } + tokio::time::sleep(CONNECT_RETRY_BACKOFF * attempt as u32).await; + } + } + pub async fn send_last_frame(&self) -> Result<(), VolcengineASRError> { // 等所有 fire-and-forget 发送完成。否则末帧(NegativeSequence)可能比尾部 // chunk 先到服务端,被识别为「流已结束」之后再到的 chunk 全部丢弃 = 尾句吞掉。 diff --git a/openless-all/app/src-tauri/src/coding_agent/stream.rs b/openless-all/app/src-tauri/src/coding_agent/stream.rs index d1349eb0..78b8d722 100644 --- a/openless-all/app/src-tauri/src/coding_agent/stream.rs +++ b/openless-all/app/src-tauri/src/coding_agent/stream.rs @@ -15,6 +15,8 @@ pub enum CodingAgentEvent { Delta { session_id: String, text: String }, /// agent 触发了某个工具(如 Bash / Edit)。 ToolUse { session_id: String, name: String }, + /// 会话上下文被压缩(`system/compact_boundary`)。前端在消息流对应位置内嵌提示。 + Compaction { session_id: String }, /// 运行完成的最终结果。 Completed { session_id: String, @@ -65,6 +67,16 @@ pub fn parse_stream_json_line(session_id: &str, line: &str) -> Option { + // 上下文压缩:headless claude 压缩会话历史时发一行 system/compact_boundary。 + // 其余 system 行(init 等)继续忽略。 + if v.get("subtype")?.as_str()? != "compact_boundary" { + return None; + } + Some(CodingAgentEvent::Compaction { + session_id: session_id.to_string(), + }) + } "result" => { let is_error = v.get("is_error").and_then(|b| b.as_bool()).unwrap_or(false); let text = v @@ -154,6 +166,17 @@ mod tests { ); } + #[test] + fn parses_compact_boundary() { + let line = r#"{"type":"system","subtype":"compact_boundary","compact_metadata":{"trigger":"auto","pre_tokens":50000}}"#; + assert_eq!( + parse_stream_json_line("s1", line), + Some(CodingAgentEvent::Compaction { + session_id: "s1".into() + }) + ); + } + #[test] fn ignores_system_init_and_garbage() { assert_eq!( diff --git a/openless-all/app/src-tauri/src/commands/history.rs b/openless-all/app/src-tauri/src/commands/history.rs index e8fa29ff..1f6a84ab 100644 --- a/openless-all/app/src-tauri/src/commands/history.rs +++ b/openless-all/app/src-tauri/src/commands/history.rs @@ -15,6 +15,18 @@ pub fn clear_history(coord: CoordinatorState<'_>) -> Result<(), String> { coord.history().clear().map_err(|e| e.to_string()) } +/// 每日活动计数(日期升序),概览页年度热力图的数据源。与历史内容 / 保留策略解耦: +/// 清空历史不影响它,全年格子照亮。 +#[tauri::command] +pub fn get_activity_stats(coord: CoordinatorState<'_>) -> Vec { + coord + .activity() + .snapshot() + .into_iter() + .map(|(date, count)| ActivityDay { date, count }) + .collect() +} + /// 读取某次会话的原始麦克风 wav 字节流。文件存在的条件:debug 用户的任意会话,或任意 /// 「转录失败 / empty」会话(失败保留)——成功的非 debug 会话录音会在插入后删掉。 /// 文件名规约:`/recordings/.wav`,与 DictationSession.id 同名。 diff --git a/openless-all/app/src-tauri/src/commands/mod.rs b/openless-all/app/src-tauri/src/commands/mod.rs index cfb150be..bac691e0 100644 --- a/openless-all/app/src-tauri/src/commands/mod.rs +++ b/openless-all/app/src-tauri/src/commands/mod.rs @@ -56,7 +56,8 @@ pub(crate) use crate::recorder::{AudioConsumer, Recorder}; #[cfg(not(mobile))] pub(crate) use crate::types::WindowsImeStatus; pub(crate) use crate::types::{ - builtin_style_pack_id, default_active_style_pack_id, AndroidAccessibilityStatus, + builtin_style_pack_id, default_active_style_pack_id, ActivityDay, + AndroidAccessibilityStatus, AndroidOverlayStatus, ChineseScriptPreference, ComboBinding, CorrectionRule, CredentialsStatus, DictationSession, DictionaryEntry, HotkeyCapability, HotkeyStatus, OutputLanguagePreference, PolishMode, ShortcutBinding, StylePack, StylePackKind, StylePackRuntimeDiagnostics, diff --git a/openless-all/app/src-tauri/src/commands/qa.rs b/openless-all/app/src-tauri/src/commands/qa.rs index e4558b64..a125b8a0 100644 --- a/openless-all/app/src-tauri/src/commands/qa.rs +++ b/openless-all/app/src-tauri/src/commands/qa.rs @@ -77,6 +77,12 @@ pub fn less_computer_window_resize(coord: CoordinatorState<'_>, height: f64) { coord.less_computer_window_resize(height); } +/// 浮窗打字输入:文字指令直接进入 Less Computer 执行链(跳过录音与 ASR)。 +#[tauri::command] +pub fn less_computer_submit_text(coord: CoordinatorState<'_>, text: String) { + coord.less_computer_submit_text(text); +} + /// 内联审批卡的 Approve / Deny 回执。token 关联到等待中的拦截动作。 /// /// 安全:审批 UI 渲染在 less-computer 窗口(LessComputerPanel),故仅允许该窗口提交, diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 9acfd322..ec296073 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -39,8 +39,8 @@ use crate::correction::apply_correction_rules; use crate::hotkey::{HotkeyEvent, HotkeyMonitor}; use crate::insertion::TextInserter; use crate::persistence::{ - sync_style_pack_preferences, CorrectionRuleStore, CredentialAccount, CredentialsVault, - DictionaryStore, HistoryStore, PreferencesStore, StylePackStore, + sync_style_pack_preferences, ActivityStore, CorrectionRuleStore, CredentialAccount, + CredentialsVault, DictionaryStore, HistoryStore, PreferencesStore, StylePackStore, }; use crate::llm_gemini::{GeminiConfig, GeminiProvider}; @@ -238,6 +238,8 @@ pub struct Coordinator { struct Inner { app: Mutex>, history: HistoryStore, + /// 每日活动计数(热力图数据源),与 history 的保留策略解耦。 + activity: ActivityStore, prefs: PreferencesStore, style_packs: StylePackStore, vocab: DictionaryStore, @@ -305,6 +307,11 @@ struct Inner { /// 最近一次应用到 capsule 窗口的几何状态。避免录音 level tick 反复触发 /// resize / reposition。 capsule_layout: Mutex>, + /// 预备态标志:按下热键即"乐观显示"胶囊(带入场动画),此时麦克风还在 cpal + /// init 窗口内、没有第一帧 PCM。为 true 时 emit_capsule 把 Recording payload 的 + /// `warming` 打成 true(前端渲染"待命"光效);`level_handler` 首次触发(PCM 真的 + /// 流入)后置 false,光条"点亮"进入正式录音。begin_session 每次入场重置为 true。 + capsule_warming: AtomicBool, /// QA 用的 ASR 句柄。必须跟 active_asr_provider 保持一致,避免浮窗走不同入口。 qa_asr: Mutex>, /// QA 用的 Recorder 句柄。 @@ -383,10 +390,16 @@ impl Coordinator { CorrectionRuleStore::new_fallback() }); + let activity = ActivityStore::load().unwrap_or_else(|e| { + log::error!("[coord] ActivityStore init failed: {e}; 活动计数降级为内存态"); + ActivityStore::new_fallback() + }); + Self { inner: Arc::new(Inner { app: Mutex::new(None), history, + activity, prefs, style_packs, vocab, @@ -415,6 +428,7 @@ impl Coordinator { last_capsule_state: Mutex::new(None), qa_state: Mutex::new(QaSessionState::default()), capsule_layout: Mutex::new(None), + capsule_warming: AtomicBool::new(false), qa_asr: Mutex::new(None), qa_recorder: Mutex::new(None), qa_stream_cancelled: Arc::new(AtomicBool::new(false)), @@ -474,10 +488,16 @@ impl Coordinator { CorrectionRuleStore::new_fallback() }); + let activity = ActivityStore::load().unwrap_or_else(|e| { + log::error!("[coord] ActivityStore init failed: {e}; 活动计数降级为内存态"); + ActivityStore::new_fallback() + }); + Self { inner: Arc::new(Inner { app: Mutex::new(None), history, + activity, prefs, style_packs, vocab, @@ -508,6 +528,7 @@ impl Coordinator { last_capsule_state: Mutex::new(None), qa_state: Mutex::new(QaSessionState::default()), capsule_layout: Mutex::new(None), + capsule_warming: AtomicBool::new(false), qa_asr: Mutex::new(None), qa_recorder: Mutex::new(None), qa_stream_cancelled: Arc::new(AtomicBool::new(false)), @@ -1088,10 +1109,32 @@ impl Coordinator { dictation::resolve_less_computer_approval(token, approved); } + /// 浮窗打字输入:文字指令直接进入 Less Computer 执行链(与语音转写同一条 + /// 路径——同样的护栏钳制 / 审批循环 / 连续会话语义),跳过录音与 ASR。 + pub fn less_computer_submit_text(&self, text: String) { + let text = text.trim().to_string(); + if text.is_empty() { + return; + } + let inner = Arc::clone(&self.inner); + tokio::spawn(async move { + let session_id = crate::coordinator_state::new_session_id(); + if let Err(e) = + dictation::run_voice_agent_transcript(&inner, session_id, text, 0).await + { + log::warn!("[less-computer] text submit run failed: {e}"); + } + }); + } + pub fn history(&self) -> &HistoryStore { &self.inner.history } + pub fn activity(&self) -> &ActivityStore { + &self.inner.activity + } + pub fn prefs(&self) -> &PreferencesStore { &self.inner.prefs } diff --git a/openless-all/app/src-tauri/src/coordinator/asr_wiring.rs b/openless-all/app/src-tauri/src/coordinator/asr_wiring.rs index 5d9353ba..8f949f40 100644 --- a/openless-all/app/src-tauri/src/coordinator/asr_wiring.rs +++ b/openless-all/app/src-tauri/src/coordinator/asr_wiring.rs @@ -299,8 +299,16 @@ pub(super) async fn build_local_qwen3( } #[cfg(target_os = "macos")] -pub(super) fn build_apple_speech() -> Arc { - Arc::new(crate::asr::local::AppleSpeechAsr::new()) +pub(super) fn build_apple_speech( + prefs: &crate::types::UserPreferences, +) -> Arc { + // Apple 识别 locale 跟随用户工作语言主语言 —— 不显式指定 SFSpeechRecognizer 就落到 + // 系统首选语言(常是英文),中文语音会被识别成英文且理解错误。未收录语言回退默认。 + let locale = prefs + .working_languages + .first() + .and_then(|name| crate::asr::local::native_name_to_apple_locale(name)); + Arc::new(crate::asr::local::AppleSpeechAsr::new(locale)) } /// `whisper` 是 OpenAI 原生;`siliconflow` / `zhipu` / `groq` 都暴露 @@ -496,7 +504,7 @@ pub(super) async fn build_qa_asr_start(inner: &Arc, active_asr: &str) -> #[cfg(target_os = "macos")] if crate::asr::local::is_apple_speech(active_asr) { - let local = build_apple_speech(); + let local = build_apple_speech(&inner.prefs.get()); let active = ActiveAsr::AppleSpeech(Arc::clone(&local)); let consumer: Arc = local; return Ok(QaAsrStart::Ready { active, consumer }); diff --git a/openless-all/app/src-tauri/src/coordinator/capsule_focus.rs b/openless-all/app/src-tauri/src/coordinator/capsule_focus.rs index 5b7c7ddc..2398433c 100644 --- a/openless-all/app/src-tauri/src/coordinator/capsule_focus.rs +++ b/openless-all/app/src-tauri/src/coordinator/capsule_focus.rs @@ -387,11 +387,16 @@ pub(super) fn emit_capsule( inserted_chars: Option, ) { // 在 app 句柄校验之前记录,便于无 GUI 的测试断言「按下热键 → 弹了哪种胶囊」。 - *inner.last_capsule_state.lock() = Some(state); + // replace 顺带取回上一帧 state,用于判断本次是不是「入场帧」(见下方 defer_capsule_emit)。 + let prev_state = inner.last_capsule_state.lock().replace(state); let app_opt = inner.app.lock().clone(); let Some(app) = app_opt else { return }; let translation = inner.translation_modifier_seen.load(Ordering::SeqCst); let operating = inner.state.lock().voice_agent; + // 预备态只对 Recording 有意义:麦克风还没吐第一帧 PCM 时(capsule_warming=true)把 + // warming 打成 true,前端渲染「待命」光效;level_handler 首触发后翻 false → 光条点亮。 + let warming = matches!(state, CapsuleState::Recording) + && inner.capsule_warming.load(Ordering::SeqCst); let payload = CapsulePayload { state, level, @@ -400,6 +405,7 @@ pub(super) fn emit_capsule( inserted_chars, translation, operating, + warming, }; #[cfg(target_os = "android")] @@ -409,6 +415,13 @@ pub(super) fn emit_capsule( // 必须在 call-site(即音频线程触发 emit_capsule 时)就算定,否则 main thread // 闭包里读到的将是「下一帧」的 state,跟实际下发给 JS 的 payload 不一致。 let visible = !matches!(state, CapsuleState::Idle); + // 入场帧:胶囊从不可见第一次变可见。按平时的「同步 emit + 异步 show」,前端会在窗口 + // 还隐藏时就起播 capsule-in,等窗口真 show 出来动画早已播完 → 用户看到胶囊「凭空出 + // 现」而非「滑入」。修法:入场帧把发给 capsule 窗口的事件推迟到主线程闭包里、 + // window.show 之后再 emit,保证前端起播入场动画时窗口已可见、动画完整可见。Linux 不 + // 走胶囊窗口(文字经 fcitx5 直接 commit),保持原同步 emit 不变。 + let was_visible = matches!(prev_state, Some(s) if !matches!(s, CapsuleState::Idle)); + let defer_capsule_emit = visible && !was_visible && cfg!(not(target_os = "linux")); // Linux: 通过 fcitx5 插件在候选词列表下方显示听写状态,不干扰输入法预编辑。 // 只在文本变化时调用 DBus,避免录音中 ~30Hz 的音频电平回调重复调用。 @@ -509,6 +522,13 @@ pub(super) fn emit_capsule( // 闪烁。pr_agent 关注点 — 见 audit follow-up。 let inner_for_main = Arc::clone(inner); let app_for_main = app.clone(); + // 入场帧要在 window.show 之后、闭包内部把 state 回发给前端,需要 payload 的独立副本 + // move 进闭包;非入场帧走闭包外的即时同步 emit(下方),这里就是 None。 + let payload_for_deferred_emit = if defer_capsule_emit { + Some(payload.clone()) + } else { + None + }; let _ = app.run_on_main_thread(move || { let Some(window) = app_for_main.get_webview_window("capsule") else { // #470 诊断 v2:比 A/B/C 更靠前的暗点 A0 —— capsule webview 句柄取不到 @@ -568,12 +588,22 @@ pub(super) fn emit_capsule( hide_capsule_window_if_present(); let _ = window.hide(); } + // 入场帧:窗口刚 show(或本次用户关了胶囊显示走了 hide 分支),此刻再把 state 发给 + // capsule 前端 —— 前端起播 capsule-in 时窗口已可见,入场动画从头完整播放。 + if let Some(payload) = payload_for_deferred_emit.as_ref() { + let _ = app_for_main.emit_to("capsule", "capsule:state", payload); + } } }); - let _ = app.emit_to("capsule", "capsule:state", &payload); + // 非入场帧(含 Linux、录音中的 level 更新、离场/终态)保持即时同步 emit,最低延迟; + // 入场帧已在上面的主线程闭包里、window.show 之后 emit 过,这里跳过避免重复下发。 + if !defer_capsule_emit { + let _ = app.emit_to("capsule", "capsule:state", &payload); + } // 主窗口也需要 capsule:state 事件:AudioCueListener 用它触发录音提示音。 - // Linux 上胶囊隐藏时提示音仍应工作,所以同时发给 main 窗口。 + // Linux 上胶囊隐藏时提示音仍应工作,所以同时发给 main 窗口。始终即时,与胶囊窗口 + // 显示时机解耦。 let _ = app.emit_to("main", "capsule:state", &payload); } diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 922d6866..bbf82204 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -95,9 +95,10 @@ fn emit_less_computer(inner: &Arc, payload: serde_json::Value) { /// - 失败:`(raw_text, Some(reason), false)` — 流式过程出错,调用方走 raw 一次性兜底 /// - 不支持:`run_streaming_polish` 内部直接调 `polish_or_passthrough` 透明降级 /// -/// **不在流式路径里做**:`apply_chinese_script_preference` / `apply_correction_rules` -/// 这两步在 v1 跳过 —— 字符已经一边流一边落出去了,不好回退。需要的话只能关 toggle 走 -/// 一次性路径。 +/// **流式路径里的字形转换**:Simplified(t2s)在 `on_delta` 对每个 delta 就地转换 +/// (近乎逐字映射,跨 delta 拆散词条也几乎总是正确);Traditional(s2t)有真歧义, +/// `streaming_insert_eligible` 仍把它挡在一次性路径。`apply_correction_rules` 依旧 +/// 不在流式路径里做 —— 字符已经落出去,不好回退。 #[allow(clippy::too_many_arguments)] async fn run_streaming_polish( inner: &Arc, @@ -193,6 +194,18 @@ async fn run_streaming_polish( // 3. 调流式润色,on_delta 塞 mpsc;should_cancel 检查 dictation 取消旗。 let inner_for_cancel = Arc::clone(inner); let should_cancel = move || inner_for_cancel.state.lock().cancelled; + // Simplified 目标:对每个 delta 就地 t2s(转换器建一次,避免每个 delta 重新加载 + // 词典)。Traditional 不会走到这里(eligibility 已降级),Auto 无需转换。 + let delta_converter = (chinese_script_preference + == crate::types::ChineseScriptPreference::Simplified) + .then(|| { + ferrous_opencc::OpenCC::from_config(ferrous_opencc::config::BuiltinConfig::T2s) + .map_err(|e| { + log::warn!("[coord] streaming_insert: OpenCC t2s init failed, deltas stay unconverted: {e}"); + }) + .ok() + }) + .flatten(); let outcome = super::polish_or_passthrough_streaming( raw, mode, @@ -205,7 +218,11 @@ async fn run_streaming_polish( front_app, prior_turns, move |delta: &str| { - let _ = tx.send(delta.to_string()); + let converted = match delta_converter.as_ref() { + Some(converter) => converter.convert(delta), + None => delta.to_string(), + }; + let _ = tx.send(converted); }, should_cancel, ) @@ -521,10 +538,13 @@ fn streaming_insert_eligible( streaming_insert_enabled && !translation_active && (mode != PolishMode::Raw || raw_uses_llm) - // 非 Auto 字形(简/繁)要对成品文本做确定性 OpenCC 转换,而流式是边出边落字、 - // 没有成品可后处理(finalize_polished_text 在 already_streamed 时直接 return)。 - // → 非 Auto 时关掉流式,走一次性路径,确保简/繁转换真正生效(issue #643)。 - && chinese_script_preference == crate::types::ChineseScriptPreference::Auto + // 固定字形的 OpenCC 转换与流式的兼容性按方向区分: + // - Simplified(t2s):近乎逐字映射,对每个 delta 就地转换即可(跨 delta + // 边界拆散的词级条目退化为逐字转换,t2s 方向仍几乎总是正确),流式放行 + // —— 否则固定简体的用户流式静默失效且无从得知原因。 + // - Traditional(s2t):一简对多繁有真歧义(发→發/髮),需要全文上下文, + // 仍走一次性路径确保转换准确(issue #643)。 + && chinese_script_preference != crate::types::ChineseScriptPreference::Traditional && windows_insertion_allows_streaming(windows_insertion_mode) } @@ -687,7 +707,9 @@ pub(super) async fn handle_released(inner: &Arc) { } /// Less Computer 收尾:把转写当作指令交给无头 Claude,结果以胶囊展示(不插入到光标)。 -async fn run_voice_agent_transcript( +/// pub(super):除语音路径外,浮窗的打字输入(less_computer_submit_text 命令) +/// 也以文字直接进入同一条执行链(同样的护栏 / 审批 / 连续会话语义)。 +pub(super) async fn run_voice_agent_transcript( inner: &Arc, _session_id: SessionId, transcript: String, @@ -1031,6 +1053,9 @@ async fn run_less_computer_once( E::ToolUse { name, .. } => { emit_less_computer(inner, serde_json::json!({ "kind": "tool", "name": name })); } + E::Compaction { .. } => { + emit_less_computer(inner, serde_json::json!({ "kind": "compaction" })); + } E::Completed { text, cost_usd: c, .. } => { @@ -1218,6 +1243,14 @@ pub(super) async fn begin_session_as( return Ok(()); } + // 乐观显示:按下热键即弹出胶囊并播入场动画,不等麦克风/ASR。此刻麦克风还在 cpal + // init 窗口内、没有第一帧 PCM,先进「预备态」(warming=true → 前端渲染待命光效,引导 + // 用户稍候再开口);level_handler 首次触发(PCM 真的流入)后翻成正式录音态、光条点亮。 + // 这样把「视觉反馈」与「麦克风就绪」解耦:即时反馈 + 完整入场动画,同时用预备→点亮的 + // 过渡守住「不漏首字」。若随后凭证/权限校验失败,下面分支会用 Error 覆盖这一帧。 + inner.capsule_warming.store(true, Ordering::SeqCst); + emit_capsule(inner, CapsuleState::Recording, 0.0, 0, None, None); + if let Err(message) = ensure_asr_credentials() { log::warn!("[coord] ASR credential gate failed: {message}"); emit_capsule( @@ -1384,7 +1417,7 @@ pub(super) async fn begin_session_as( .await?; } MacosKeylessDictationProvider::AppleSpeech => { - let local = build_apple_speech(); + let local = build_apple_speech(&inner.prefs.get()); store_asr_for_session( inner, current_session_id, @@ -1629,6 +1662,10 @@ pub(super) async fn start_recorder_for_starting( .started_at .elapsed() .as_millis() as u64; + // 第一帧 PCM 真的流到 consumer 了(recorder.rs::process_callback 的顺序保证 + // consume_pcm_chunk 先于 level_handler)——关掉预备态,让这一帧起 payload.warming + // 翻 false,前端把「待命」光条点亮成正式录音态。之后每帧都是 false(幂等)。 + inner_for_level.capsule_warming.store(false, Ordering::SeqCst); emit_capsule( &inner_for_level, CapsuleState::Recording, @@ -2729,6 +2766,14 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { ) { log::error!("[coord] history append failed: {e}"); } + // 活动计数(概览页热力图数据源):只有成功完成的听写才点亮格子——转录失败 / + // 错误收尾的两处 append 不计。写失败不阻断主流程。 + if let Err(e) = inner + .activity + .bump(&chrono::Local::now().format("%Y-%m-%d").to_string()) + { + log::warn!("[coord] activity bump failed: {e}"); + } // 远程输入:把本次最终文字回传给手机端。remote_server 的 WS handler 订阅了 // "remote:result"(mod.rs:614),但此前全仓从未 emit,导致手机结果区永远空(#691)。 @@ -3268,30 +3313,31 @@ mod tests { } #[test] - fn streaming_disabled_for_non_auto_script_so_opencc_runs() { - // issue #643:非 Auto 字形(简/繁)必须走一次性路径,让 finalize 的 OpenCC 转换生效。 + fn streaming_script_gate_blocks_only_traditional() { + // Traditional(s2t)有一简对多繁的真歧义,必须走一次性路径做全文 OpenCC + // 转换(issue #643);Simplified(t2s)近乎逐字,on_delta 就地转换即可, + // 不再挡流式(用户反馈:固定简体导致流式静默失效)。 + assert!(!streaming_insert_eligible( + true, + false, + PolishMode::Light, + false, + ChineseScriptPreference::Traditional, + crate::types::WindowsInsertionMode::SendInput, + )); for pref in [ + ChineseScriptPreference::Auto, ChineseScriptPreference::Simplified, - ChineseScriptPreference::Traditional, ] { - assert!(!streaming_insert_eligible( + assert!(streaming_insert_eligible( true, false, PolishMode::Light, false, pref, - crate::types::WindowsInsertionMode::Tsf, + crate::types::WindowsInsertionMode::SendInput, )); } - // Auto + SendInput 仍可流式。 - assert!(streaming_insert_eligible( - true, - false, - PolishMode::Light, - false, - ChineseScriptPreference::Auto, - crate::types::WindowsInsertionMode::SendInput, - )); } #[test] diff --git a/openless-all/app/src-tauri/src/coordinator/qa_session.rs b/openless-all/app/src-tauri/src/coordinator/qa_session.rs index 8e9664c1..094a1c6c 100644 --- a/openless-all/app/src-tauri/src/coordinator/qa_session.rs +++ b/openless-all/app/src-tauri/src/coordinator/qa_session.rs @@ -690,6 +690,10 @@ pub(super) async fn begin_qa_session(inner: &Arc) -> Result<(), String> { return Ok(()); } + // QA 无「预备态」语义(不走等麦克风预热的乐观显示),显式清掉 capsule_warming —— + // 否则若上一次听写在拿到首帧 PCM 前异常早退、warming 停在 true,这里的 QA 录音胶囊会 + // 读到陈旧 true 卡在「待命」收拢态(QA 的 level_handler 不翻这个标志)。审核 follow-up。 + inner.capsule_warming.store(false, Ordering::SeqCst); // 显式弹胶囊到 Recording。level_handler 后续会持续推电平,胶囊里"录音中…" // 的视觉反馈跟主听写完全一致。 emit_capsule(inner, CapsuleState::Recording, 0.0, 0, None, None); diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 369ee0c6..4035c063 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -166,6 +166,7 @@ macro_rules! app_invoke_handler_desktop { commands::list_history, commands::delete_history_entry, commands::clear_history, + commands::get_activity_stats, commands::read_audio_recording, commands::retranscribe_recording, commands::marketplace_list, @@ -232,6 +233,7 @@ macro_rules! app_invoke_handler_desktop { commands::qa_window_pin, commands::less_computer_window_dismiss, commands::less_computer_window_resize, + commands::less_computer_submit_text, commands::less_computer_approve, commands::validate_combo_hotkey, commands::set_combo_hotkey, @@ -331,6 +333,7 @@ macro_rules! app_invoke_handler_mobile { $crate::commands::list_history, $crate::commands::delete_history_entry, $crate::commands::clear_history, + $crate::commands::get_activity_stats, $crate::commands::read_audio_recording, $crate::commands::retranscribe_recording, $crate::commands::marketplace_list, @@ -509,6 +512,11 @@ fn run_desktop() { Err(e) => log::warn!("[capsule] to_panel failed: {e:?}"), } } + // 纯光效舞台没有任何可点元素(✕/✓ 按钮已移除),而窗口放大到 460×180 + // 盖住屏幕底部中央 —— 必须鼠标穿透,否则会挡住底下应用的点击。 + if let Err(e) = capsule.set_ignore_cursor_events(true) { + log::warn!("[capsule] set_ignore_cursor_events failed: {e}"); + } if let Err(e) = position_capsule_bottom_center(&capsule, false) { log::warn!("[capsule] position failed: {e}"); } @@ -2080,11 +2088,21 @@ fn make_qa_window_draggable_macos(window: &tauri::WebviewWind log::warn!("[qa] ns_window null; drag fix skipped"); return; } - unsafe { - let _: () = msg_send![ns_window, setMovableByWindowBackground: Bool::YES]; - let _: () = msg_send![ns_window, setMovable: Bool::YES]; + // 异常兜底:AppKit 在 drag margins 状态不一致时会从 setMovable 内部 raise + // NSException;不捕获的话异常穿过 Rust 边界直接 abort 整个 app。捕获后降级为 + // 日志(拖动失效但 app 活着),日志会指认具体异常便于追根因。 + // SAFETY: 闭包内只有两个无返回值的 ObjC 消息发送,没有需要运行析构的 Rust 值, + // 异常展开跳过闭包帧不会破坏内存安全。 + let result = unsafe { + objc2::exception::catch(std::panic::AssertUnwindSafe(|| { + let _: () = msg_send![ns_window, setMovableByWindowBackground: Bool::YES]; + let _: () = msg_send![ns_window, setMovable: Bool::YES]; + })) + }; + match result { + Ok(()) => log::info!("[qa] NSWindow movableByWindowBackground=YES"), + Err(e) => log::error!("[qa] drag setup raised ObjC exception (caught, drag disabled): {e:?}"), } - log::info!("[qa] NSWindow movableByWindowBackground=YES"); } /// 懒创建 QA 浮窗:原来在 tauri.conf.json eager 创建(常驻一个 WebKit 进程)。改为首次 @@ -2112,8 +2130,15 @@ fn ensure_qa_window(app: &AppHandle) -> Option { + // ⚠️ NSWindow 操作必须在主线程(macOS 26 硬约束)。ensure_qa_window 常从 + // tokio worker 进来(热键桥接循环),在 worker 线程直接 setMovable 会让 + // AppKit 从 _postWindowNeedsToResetDragMargins 抛 NSException 直接崩掉 + // 整个 app(2026-07-03 四次同栈崩溃的根因)——dispatch 回主线程执行。 #[cfg(target_os = "macos")] - make_qa_window_draggable_macos(&w); + { + let w_clone = w.clone(); + let _ = app.run_on_main_thread(move || make_qa_window_draggable_macos(&w_clone)); + } Some(w) } Err(e) => { @@ -2625,42 +2650,21 @@ struct CapsuleWindowBounds { } fn capsule_window_bounds(translation_active: bool) -> CapsuleWindowBounds { - #[cfg(target_os = "windows")] - { - const WINDOWS_CAPSULE_PILL_WIDTH: f64 = 196.0; - const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12.0; - CapsuleWindowBounds { - // Keep the existing Windows hitbox width, but express it as - // pill width (196) + symmetric 12px side insets for shadow room. - width: WINDOWS_CAPSULE_PILL_WIDTH + WINDOWS_CAPSULE_SIDE_INSET * 2.0, - height: if translation_active { 118.0 } else { 84.0 }, - bottom_inset: 12.0, - } - } - - #[cfg(not(target_os = "windows"))] - { - // macOS / Linux:固定 220×110,与 1.2.11 行为一致 — 录音 / 翻译徽章 - // 共用同一个窗口尺寸,避免按 Shift 后窗口高度变化导致胶囊整体下移。 - let _ = translation_active; - CapsuleWindowBounds { - width: 220.0, - height: 110.0, - bottom_inset: 0.0, - } + // 纯光效语音舞台(siri-glsl 原始比例):光条横贯 ~420px + 发光扩散余量。 + // 与前端 src/lib/capsuleLayout.ts 的 VOICE_ORB_STAGE_* 保持一致。 + let _ = translation_active; + CapsuleWindowBounds { + width: 460.0, + height: 180.0, + bottom_inset: 0.0, } } fn capsule_visual_height(_translation_active: bool) -> f64 { - #[cfg(target_os = "windows")] - { - 52.0 - } - - #[cfg(not(target_os = "windows"))] - { - 96.0 - } + // 故意小于窗口高(180):窗口整体下沉 40px,让光条视觉中心落回原胶囊的高度 + // 附近(bottom_visual_position 以此值算窗口顶 y = 屏底 - 80 - visual_height)。 + // 底部 40px 只是发光余量,窗口透明 + 鼠标穿透,压到 Dock 上方无碍。 + 140.0 } fn capsule_height_for_qa() -> f64 { @@ -2729,53 +2733,31 @@ mod tests { } #[test] - fn capsule_window_bounds_leave_room_for_windows_shadow() { + fn capsule_window_bounds_match_voice_orb_stage() { let bounds = capsule_window_bounds(false); - #[cfg(target_os = "windows")] - assert_eq!( - (bounds.width, bounds.height, bounds.bottom_inset), - (220.0, 84.0, 12.0) - ); - - #[cfg(not(target_os = "windows"))] assert_eq!( (bounds.width, bounds.height, bounds.bottom_inset), - (220.0, 110.0, 0.0) + (460.0, 180.0, 0.0) ); } #[test] - fn capsule_window_bounds_expand_for_translation_badge() { + fn capsule_window_bounds_stay_fixed_for_translation_badge() { let bounds = capsule_window_bounds(true); - #[cfg(target_os = "windows")] assert_eq!( (bounds.width, bounds.height, bounds.bottom_inset), - (220.0, 118.0, 12.0) - ); - - #[cfg(not(target_os = "windows"))] - assert_eq!( - (bounds.width, bounds.height, bounds.bottom_inset), - (220.0, 110.0, 0.0) + (460.0, 180.0, 0.0) ); } #[test] - fn capsule_visual_height_matches_frontend_pill() { - #[cfg(target_os = "windows")] - assert_eq!(capsule_visual_height(true), 52.0); - - #[cfg(not(target_os = "windows"))] - assert_eq!(capsule_visual_height(true), 96.0); + fn capsule_visual_height_sinks_stage_toward_dock() { + assert_eq!(capsule_visual_height(true), 140.0); } #[test] fn qa_anchor_uses_normal_capsule_height_source() { - #[cfg(target_os = "windows")] - assert_eq!(capsule_height_for_qa(), 52.0); - - #[cfg(not(target_os = "windows"))] - assert_eq!(capsule_height_for_qa(), 96.0); + assert_eq!(capsule_height_for_qa(), 140.0); } #[test] diff --git a/openless-all/app/src-tauri/src/persistence/activity.rs b/openless-all/app/src-tauri/src/persistence/activity.rs new file mode 100644 index 00000000..35c9aa85 --- /dev/null +++ b/openless-all/app/src-tauri/src/persistence/activity.rs @@ -0,0 +1,69 @@ +//! 每日听写活动计数(`date(YYYY-MM-DD) → count`),供概览页年度活动热力图使用。 +//! +//! 与历史内容存储完全解耦:不含任何转写文本,也不受历史保留策略 / 条数上限影响 +//! —— 清理历史不会抹掉活动足迹,热力图因此能覆盖全年而无需放开历史上限 +//! (取代 PR #716 里「为热力图把历史改为无限保留」的方案)。 +//! 写入时按保留窗口(两年)裁剪最早的日期,文件天然有界。 + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use anyhow::Result; +use parking_lot::Mutex; + +use super::{atomic_write, data_dir, ensure_dir, read_or_default}; + +const ACTIVITY_FILE: &str = "activity.json"; +/// 保留最近两年(含闰年余量)的日计数,超窗的最早日期在写入时移除。 +const ACTIVITY_RETENTION_DAYS: usize = 731; + +pub struct ActivityStore { + path: PathBuf, + cache: Mutex>, +} + +impl ActivityStore { + pub fn load() -> Result { + let dir = data_dir()?; + ensure_dir(&dir)?; + let path = dir.join(ACTIVITY_FILE); + let cache: BTreeMap = read_or_default(&path)?; + Ok(Self { + path, + cache: Mutex::new(cache), + }) + } + + /// load 失败时的内存降级:计数仍可累加(本次运行内有效),写盘静默失败。 + /// 活动计数是非关键路径,不因它阻断听写初始化。 + pub fn new_fallback() -> Self { + Self { + path: PathBuf::new(), + cache: Mutex::new(BTreeMap::new()), + } + } + + /// 记录一次活动。`date` 为本地日期 `YYYY-MM-DD`(BTreeMap 按字典序即按日期序)。 + pub fn bump(&self, date: &str) -> Result<()> { + let mut cache = self.cache.lock(); + *cache.entry(date.to_string()).or_insert(0) += 1; + while cache.len() > ACTIVITY_RETENTION_DAYS { + let oldest = match cache.keys().next() { + Some(key) => key.clone(), + None => break, + }; + cache.remove(&oldest); + } + let bytes = serde_json::to_vec_pretty(&*cache)?; + atomic_write(&self.path, &bytes) + } + + /// 全量快照(日期升序),前端聚合成热力图。 + pub fn snapshot(&self) -> Vec<(String, u32)> { + self.cache + .lock() + .iter() + .map(|(date, count)| (date.clone(), *count)) + .collect() + } +} diff --git a/openless-all/app/src-tauri/src/persistence/credentials.rs b/openless-all/app/src-tauri/src/persistence/credentials.rs index 6e160ec7..22dd7e27 100644 --- a/openless-all/app/src-tauri/src/persistence/credentials.rs +++ b/openless-all/app/src-tauri/src/persistence/credentials.rs @@ -451,13 +451,62 @@ fn read_chunk_manifest(json: &str) -> Option { } } +/// Windows Credential Manager (`CredReadW`) can transiently fail right after +/// login / under contention when we read the manifest entry plus every chunk +/// entry in quick succession. A single failed read makes the whole credential +/// set look empty → `load_keyring_credentials` returns `Err` → `load_credentials` +/// falls back to an empty default → Overview shows「火山引擎未配置」even though the +/// secrets are present (the next dictation re-reads and succeeds, which is why the +/// bug is *probabilistic* and the app "实际可以正常使用"). The more chunks a +/// credential set spans, the more reads per load, the higher the odds at least +/// one trips. Retry transient errors a few times with short backoff. +/// +/// macOS / Linux keep the original single-shot behavior on purpose: their read +/// errors are ACL denials that won't heal on retry, and the un-cached error path +/// already retries on the next call — adding sleeps there would only slow the +/// macOS first-launch Keychain authorization flow. +#[cfg(target_os = "windows")] +const KEYRING_READ_RETRY_ATTEMPTS: usize = 4; +#[cfg(target_os = "windows")] +const KEYRING_READ_RETRY_BACKOFF_MS: u64 = 60; + #[cfg(not(target_os = "android"))] fn get_keyring_password(account: &str) -> Result> { - match keyring_entry_for(account)?.get_password() { - Ok(value) => Ok(Some(value)), - Err(keyring::Error::NoEntry) => Ok(None), - Err(e) => { - Err(anyhow!(e)).with_context(|| format!("read system credential vault {account}")) + #[cfg(target_os = "windows")] + { + let mut attempt = 0usize; + loop { + match keyring_entry_for(account)?.get_password() { + Ok(value) => return Ok(Some(value)), + // NoEntry is a definitive "not stored" answer, never a transient + // failure — return immediately so genuinely-unconfigured providers + // don't pay the retry latency. + Err(keyring::Error::NoEntry) => return Ok(None), + Err(e) => { + attempt += 1; + if attempt >= KEYRING_READ_RETRY_ATTEMPTS { + return Err(anyhow!(e)) + .with_context(|| format!("read system credential vault {account}")); + } + log::warn!( + "[vault] transient credential read for {account} failed \ + (attempt {attempt}/{KEYRING_READ_RETRY_ATTEMPTS}): {e}; retrying" + ); + std::thread::sleep(std::time::Duration::from_millis( + KEYRING_READ_RETRY_BACKOFF_MS * attempt as u64, + )); + } + } + } + } + #[cfg(not(target_os = "windows"))] + { + match keyring_entry_for(account)?.get_password() { + Ok(value) => Ok(Some(value)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(e) => { + Err(anyhow!(e)).with_context(|| format!("read system credential vault {account}")) + } } } } diff --git a/openless-all/app/src-tauri/src/persistence/mod.rs b/openless-all/app/src-tauri/src/persistence/mod.rs index 0e99f47a..51e0da59 100644 --- a/openless-all/app/src-tauri/src/persistence/mod.rs +++ b/openless-all/app/src-tauri/src/persistence/mod.rs @@ -24,6 +24,7 @@ use anyhow::{Context, Result}; use serde::Deserialize; use uuid::Uuid; +mod activity; mod correction; mod credentials; mod dictionary; @@ -32,6 +33,7 @@ mod paths; mod preferences; mod style_pack; +pub use activity::*; pub use correction::*; pub use credentials::*; pub use dictionary::*; diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index 0de6b479..69d6a9c8 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -127,6 +127,14 @@ pub enum InsertStatus { Failed, } +/// 概览页年度活动热力图的单日计数(date = 本地日期 YYYY-MM-DD)。 +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActivityDay { + pub date: String, + pub count: u32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DictationSession { @@ -819,6 +827,10 @@ pub struct UserPreferences { /// 默认 true(更接近用户习惯)。 #[serde(default = "default_true")] pub streaming_insert_save_clipboard: bool, + /// 概览页是否显示「年度活动」热力图卡。默认 true;关闭只隐藏卡片, + /// 活动计数照常记录(persistence/activity.rs),再打开时全年数据仍在。 + #[serde(default = "default_true")] + pub show_overview_activity_heatmap: bool, /// 主窗口启动 + 后台每 60 分钟自动检查更新。默认 true。 /// Android 开启后自动检查并下载,校验后打开系统安装器;桌面仅自动检查 + 用户确认安装。 /// 关闭后仅 Settings 手动「检查更新」按钮可用。 @@ -1039,6 +1051,8 @@ struct UserPreferencesWire { #[serde(default = "default_true")] streaming_insert_save_clipboard: bool, #[serde(default = "default_true")] + show_overview_activity_heatmap: bool, + #[serde(default = "default_true")] auto_update_check: bool, #[serde(default)] history_max_entries: Option, @@ -1133,6 +1147,7 @@ impl Default for UserPreferencesWire { streaming_insert: prefs.streaming_insert, streaming_insert_default_migrated: prefs.streaming_insert_default_migrated, streaming_insert_save_clipboard: prefs.streaming_insert_save_clipboard, + show_overview_activity_heatmap: prefs.show_overview_activity_heatmap, auto_update_check: prefs.auto_update_check, history_max_entries: prefs.history_max_entries, record_audio_for_debug: prefs.record_audio_for_debug, @@ -1251,6 +1266,7 @@ impl<'de> Deserialize<'de> for UserPreferences { streaming_insert, streaming_insert_default_migrated: true, streaming_insert_save_clipboard: wire.streaming_insert_save_clipboard, + show_overview_activity_heatmap: wire.show_overview_activity_heatmap, auto_update_check: wire.auto_update_check, history_max_entries: wire.history_max_entries, record_audio_for_debug: wire.record_audio_for_debug, @@ -1991,6 +2007,7 @@ impl Default for UserPreferences { streaming_insert: true, streaming_insert_default_migrated: true, streaming_insert_save_clipboard: true, + show_overview_activity_heatmap: true, auto_update_check: true, history_max_entries: None, record_audio_for_debug: false, @@ -2658,6 +2675,12 @@ pub struct CapsulePayload { /// 从 "thinking" 换成 "using"——告诉用户 Agent 正在操作电脑而非单纯思考。 #[serde(default)] pub operating: bool, + /// 预备态:胶囊已经"乐观显示"出来(按下热键即弹出并播入场动画),但麦克风还没 + /// 真正开始 capture 第一帧 PCM。为 true 时前端渲染"待命"光效(柔和呼吸、不接真实 + /// 电平),并暗示用户先别急着开口;`level_handler` 首次触发(PCM 真的流入)后翻成 + /// false,光条"点亮"进入正式录音态。只对 Recording 状态有意义。详见胶囊出现时序改造。 + #[serde(default)] + pub warming: bool, } /// Snapshot of credentials read from vault — only what the UI needs to know diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index 7c12331e..6f2de3b8 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenLess", - "version": "1.3.13-Beta.1", + "version": "1.3.14", "identifier": "com.openless.app", "build": { "beforeDevCommand": "npm run dev", @@ -34,8 +34,8 @@ "label": "capsule", "url": "index.html?window=capsule", "title": "OpenLess Capsule", - "width": 220, - "height": 110, + "width": 460, + "height": 180, "decorations": false, "transparent": true, "shadow": false, diff --git a/openless-all/app/src/App.tsx b/openless-all/app/src/App.tsx index 28bdb085..2801e198 100644 --- a/openless-all/app/src/App.tsx +++ b/openless-all/app/src/App.tsx @@ -53,7 +53,7 @@ const ANDROID_SETUP_WIZARD_COMPLETE_KEY = 'openless.androidSetupWizardComplete'; export function App({ isCapsule, isQa, isLessComputer, isLessComputerGlow, forcedOs }: AppProps) { if (isCapsule) { - return ; + return ; } if (isQa) { return ( diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index 1c8842c9..0281a645 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -303,7 +303,7 @@ export function UpdateDialog({ const androidInstalled = isAndroid() && status === 'downloaded'; return (
-
+
{t(`settings.about.updateDialog.${status}.title`)}
{androidInstalled diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index bfbc7135..3143b9ac 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -1,38 +1,32 @@ -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { detectOS, type OS } from './WindowChrome'; +import { SiriGL, warmUpSiriShaders } from './SiriGL'; import { getCapsuleHostMetrics, - getCapsuleMessageLayout, getCapsulePillMetrics, } from '../lib/capsuleLayout'; -import { invokeOrMock, isTauri } from '../lib/ipc'; +import { isTauri } from '../lib/ipc'; import type { CapsulePayload, CapsuleState } from '../lib/types'; // 胶囊 keyframes 注入一次到 document.head,而不是放在组件 JSX 里。否则录音时音量 // 每帧(~60Hz)setLevel 都会让 React 重新创建/reconcile 这个