diff --git a/package-lock.json b/package-lock.json index fc2b345..7690559 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,17 @@ { "name": "metona-ollama-desktop", - "version": "3.3.0", + "version": "3.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metona-ollama-desktop", - "version": "3.3.0", + "version": "3.3.4", "license": "MIT", + "dependencies": { + "@types/better-sqlite3": "^7.6.13", + "better-sqlite3": "^12.9.0" + }, "devDependencies": { "@types/node": "^20.17.0", "electron": "^33.4.11", @@ -1420,6 +1424,15 @@ "node": ">= 10" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1488,7 +1501,6 @@ "version": "20.19.39", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1933,7 +1945,6 @@ "version": "1.5.1", "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -1950,11 +1961,33 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -2005,7 +2038,6 @@ "version": "5.7.1", "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2649,7 +2681,6 @@ "version": "6.0.0", "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -2665,7 +2696,6 @@ "version": "3.1.0", "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2674,6 +2704,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", @@ -2756,7 +2795,6 @@ "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3176,7 +3214,6 @@ "version": "1.4.5", "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -3319,6 +3356,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -3382,6 +3428,12 @@ "pend": "~1.2.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", @@ -3473,9 +3525,7 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "8.1.0", @@ -3623,6 +3673,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", @@ -3953,7 +4009,6 @@ "version": "1.2.1", "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -4013,7 +4068,12 @@ "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/ip-address": { @@ -4533,7 +4593,6 @@ "version": "1.2.8", "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4649,6 +4708,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -4675,6 +4740,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", @@ -4689,7 +4760,6 @@ "version": "3.89.0", "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -4702,7 +4772,6 @@ "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4853,7 +4922,6 @@ "version": "1.4.0", "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5075,6 +5143,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5118,7 +5213,6 @@ "version": "3.0.4", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -5148,6 +5242,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -5165,7 +5274,6 @@ "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -5377,7 +5485,6 @@ "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -5493,6 +5600,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -5655,7 +5807,6 @@ "version": "1.3.0", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -5719,6 +5870,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", @@ -5763,13 +5923,29 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -5870,6 +6046,18 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", @@ -5902,7 +6090,6 @@ "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -5962,7 +6149,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/verror": { @@ -6118,7 +6304,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xmlbuilder": { diff --git a/package.json b/package.json index ae0b702..679f272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ollama-desktop", - "version": "3.3.4", + "version": "4.0.0", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "main": "dist/main/main.js", "author": "thzxx", @@ -31,7 +31,9 @@ "target": [ { "target": "nsis", - "arch": ["x64"] + "arch": [ + "x64" + ] } ], "icon": "assets/icons/llama.ico", @@ -54,5 +56,9 @@ "electron-builder": "^25.1.8", "typescript": "^5.7.0", "vite": "^5.4.0" + }, + "dependencies": { + "@types/better-sqlite3": "^7.6.13", + "better-sqlite3": "^12.9.0" } } diff --git a/src/main/db/sqlite.ts b/src/main/db/sqlite.ts new file mode 100644 index 0000000..27c8f83 --- /dev/null +++ b/src/main/db/sqlite.ts @@ -0,0 +1,431 @@ +/** + * Metona Ollama Desktop - SQLite 数据库层 + * v4.0: 替代 IndexedDB,使用 better-sqlite3 + */ + +import Database from 'better-sqlite3'; +import * as path from 'path'; +import { app } from 'electron'; + +let db: Database.Database | null = null; + +/** 获取数据库实例 */ +export function getDb(): Database.Database { + if (!db) throw new Error('数据库未初始化,请先调用 initDatabase()'); + return db; +} + +/** 初始化数据库 */ +export function initDatabase(): Database.Database { + if (db) return db; + + const dbPath = path.join(app.getPath('userData'), 'metona.db'); + db = new Database(dbPath); + + // 性能优化 + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.pragma('foreign_keys = ON'); + + // 创建表 + db.exec(` + -- 会话表 + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + model TEXT NOT NULL, + system_prompt TEXT, + parent_id TEXT, + status TEXT DEFAULT 'active', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (parent_id) REFERENCES sessions(id) + ); + + -- 消息表 + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT, + thinking TEXT, + images TEXT, + tool_calls TEXT, + tool_name TEXT, + eval_count INTEGER, + total_duration INTEGER, + created_at INTEGER NOT NULL, + FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at); + + -- 工具调用记录表 + CREATE TABLE IF NOT EXISTS tool_calls ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + session_id TEXT NOT NULL, + tool_name TEXT NOT NULL, + arguments TEXT, + result TEXT, + status TEXT DEFAULT 'pending', + duration_ms INTEGER, + created_at INTEGER NOT NULL, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, tool_name); + + -- 记忆表 + CREATE TABLE IF NOT EXISTS memories ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + content TEXT NOT NULL, + importance INTEGER DEFAULT 5, + tags TEXT, + source TEXT, + session_id TEXT, + use_count INTEGER DEFAULT 0, + embedding TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_used_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type); + CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC); + + -- 记忆 FTS5 全文搜索 + CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( + content, tags, type, + content='memories', + content_rowid='rowid' + ); + + -- 设置表 + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + + -- 执行轨迹表(Agent 可观测性) + CREATE TABLE IF NOT EXISTS traces ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + step_index INTEGER, + thought TEXT, + action TEXT, + action_input TEXT, + observation TEXT, + loop_count INTEGER, + created_at INTEGER NOT NULL, + FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_id, created_at); + `); + + return db; +} + +// ─── Sessions CRUD ─── + +export interface SessionRow { + id: string; + title: string; + model: string; + system_prompt: string | null; + parent_id: string | null; + status: string; + created_at: number; + updated_at: number; +} + +export interface MessageRow { + id: string; + session_id: string; + role: string; + content: string | null; + thinking: string | null; + images: string | null; + tool_calls: string | null; + tool_name: string | null; + eval_count: number | null; + total_duration: number | null; + created_at: number; +} + +export interface MemoryRow { + id: string; + type: string; + content: string; + importance: number; + tags: string | null; + source: string | null; + session_id: string | null; + use_count: number; + embedding: string | null; + created_at: number; + updated_at: number; + last_used_at: number; +} + +export interface SettingRow { + key: string; + value: string; + updated_at: number; +} + +export interface ToolCallRow { + id: string; + message_id: string; + session_id: string; + tool_name: string; + arguments: string | null; + result: string | null; + status: string; + duration_ms: number | null; + created_at: number; +} + +export interface TraceRow { + id: string; + session_id: string; + step_index: number | null; + thought: string | null; + action: string | null; + action_input: string | null; + observation: string | null; + loop_count: number | null; + created_at: number; +} + +export function saveSession(session: SessionRow): string { + const d = getDb(); + d.prepare(`INSERT OR REPLACE INTO sessions (id, title, model, system_prompt, parent_id, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run( + session.id, session.title, session.model, session.system_prompt, session.parent_id, + session.status || 'active', session.created_at, session.updated_at + ); + return session.id; +} + +export function getSession(id: string): SessionRow | null { + const d = getDb(); + return d.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as SessionRow | null; +} + +export function getAllSessions(): SessionRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM sessions ORDER BY updated_at DESC').all() as SessionRow[]; +} + +export function deleteSession(id: string): void { + const d = getDb(); + d.prepare('DELETE FROM sessions WHERE id = ?').run(id); +} + +export function clearAllSessions(): void { + const d = getDb(); + d.exec('DELETE FROM sessions'); +} + +// ─── Messages CRUD ─── + +export function saveMessage(msg: MessageRow): string { + const d = getDb(); + d.prepare(`INSERT OR REPLACE INTO messages (id, session_id, role, content, thinking, images, tool_calls, tool_name, eval_count, total_duration, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + msg.id, msg.session_id, msg.role, msg.content, msg.thinking, msg.images, + msg.tool_calls, msg.tool_name, msg.eval_count, msg.total_duration, msg.created_at + ); + return msg.id; +} + +export function getMessagesBySession(sessionId: string): MessageRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC').all(sessionId) as MessageRow[]; +} + +export function deleteMessagesBySession(sessionId: string): void { + const d = getDb(); + d.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId); +} + +// ─── Memories CRUD ─── + +export function saveMemory(entry: MemoryRow): string { + const d = getDb(); + d.prepare(`INSERT OR REPLACE INTO memories (id, type, content, importance, tags, source, session_id, use_count, embedding, created_at, updated_at, last_used_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + entry.id, entry.type, entry.content, entry.importance, entry.tags, + entry.source, entry.session_id, entry.use_count, entry.embedding, + entry.created_at, entry.updated_at, entry.last_used_at + ); + // FTS 同步 + try { + d.prepare("DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)").run(entry.id); + d.prepare("INSERT INTO memories_fts(rowid, content, tags, type) SELECT rowid, content, tags, type FROM memories WHERE id = ?").run(entry.id); + } catch { /* FTS 同步失败不影响主流程 */ } + return entry.id; +} + +export function getMemory(id: string): MemoryRow | null { + const d = getDb(); + return d.prepare('SELECT * FROM memories WHERE id = ?').get(id) as MemoryRow | null; +} + +export function getAllMemories(): MemoryRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM memories ORDER BY importance DESC, updated_at DESC').all() as MemoryRow[]; +} + +export function getMemoriesByType(type: string): MemoryRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM memories WHERE type = ? ORDER BY importance DESC').all(type) as MemoryRow[]; +} + +export function deleteMemory(id: string): void { + const d = getDb(); + try { + d.prepare("DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)").run(id); + } catch { /* ignore */ } + d.prepare('DELETE FROM memories WHERE id = ?').run(id); +} + +export function clearAllMemories(): void { + const d = getDb(); + d.exec('DELETE FROM memories'); + try { d.exec('DELETE FROM memories_fts'); } catch { /* ignore */ } +} + +export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] { + const d = getDb(); + try { + // FTS5 搜索 + const ftsQuery = query.replace(/[^\w\u4e00-\u9fff\s]/g, ' ').split(/\s+/).filter(w => w.length > 0).map(w => `"${w}"*`).join(' OR '); + if (!ftsQuery) return []; + return d.prepare(` + SELECT m.* FROM memories m + JOIN memories_fts f ON m.rowid = f.rowid + WHERE memories_fts MATCH ? + ORDER BY rank + LIMIT ? + `).all(ftsQuery, limit) as MemoryRow[]; + } catch { + // FTS 失败回退到 LIKE 搜索 + return d.prepare(` + SELECT * FROM memories + WHERE content LIKE ? OR tags LIKE ? + ORDER BY importance DESC + LIMIT ? + `).all(`%${query}%`, `%${query}%`, limit) as MemoryRow[]; + } +} + +// ─── Settings CRUD ─── + +export function saveSetting(key: string, value: unknown): void { + const d = getDb(); + d.prepare('INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)').run( + key, JSON.stringify(value), Date.now() + ); +} + +export function getSetting(key: string, defaultValue: T | null = null): T { + const d = getDb(); + const row = d.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined; + if (!row) return defaultValue as T; + try { + return JSON.parse(row.value) as T; + } catch { + return row.value as unknown as T; + } +} + +// ─── Tool Calls CRUD ─── + +export function saveToolCall(tc: ToolCallRow): string { + const d = getDb(); + d.prepare(`INSERT OR REPLACE INTO tool_calls (id, message_id, session_id, tool_name, arguments, result, status, duration_ms, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + tc.id, tc.message_id, tc.session_id, tc.tool_name, tc.arguments, tc.result, tc.status, tc.duration_ms, tc.created_at + ); + return tc.id; +} + +export function getToolCallsBySession(sessionId: string): ToolCallRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM tool_calls WHERE session_id = ? ORDER BY created_at ASC').all(sessionId) as ToolCallRow[]; +} + +// ─── Traces CRUD ─── + +export function saveTrace(trace: TraceRow): string { + const d = getDb(); + d.prepare(`INSERT OR REPLACE INTO traces (id, session_id, step_index, thought, action, action_input, observation, loop_count, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + trace.id, trace.session_id, trace.step_index, trace.thought, trace.action, trace.action_input, trace.observation, trace.loop_count, trace.created_at + ); + return trace.id; +} + +export function getTracesBySession(sessionId: string): TraceRow[] { + const d = getDb(); + return d.prepare('SELECT * FROM traces WHERE session_id = ? ORDER BY step_index ASC').all(sessionId) as TraceRow[]; +} + +// ─── Export/Import ─── + +export interface ExportData { + sessions: SessionRow[]; + messages: MessageRow[]; + memories: MemoryRow[]; + settings: Array<{ key: string; value: unknown }>; + exportedAt: number; +} + +export function exportAllSessions(): ExportData { + const d = getDb(); + const sessions = d.prepare('SELECT * FROM sessions').all() as SessionRow[]; + const messages = d.prepare('SELECT * FROM messages').all() as MessageRow[]; + const memories = d.prepare('SELECT * FROM memories').all() as MemoryRow[]; + const settingsRows = d.prepare('SELECT * FROM settings').all() as SettingRow[]; + const settings = settingsRows.map(r => ({ key: r.key, value: (() => { try { return JSON.parse(r.value); } catch { return r.value; } })() })); + return { sessions, messages, memories, settings, exportedAt: Date.now() }; +} + +export function importSessions(data: ExportData): { imported: number; skipped: number } { + const d = getDb(); + let imported = 0; + let skipped = 0; + + const txn = d.transaction(() => { + for (const session of data.sessions) { + const existing = d.prepare('SELECT id FROM sessions WHERE id = ?').get(session.id); + if (existing) { skipped++; continue; } + saveSession(session); + imported++; + } + for (const msg of data.messages) { + const sessionExists = d.prepare('SELECT id FROM sessions WHERE id = ?').get(msg.session_id); + if (sessionExists) saveMessage(msg); + } + for (const mem of data.memories) { + const existing = d.prepare('SELECT id FROM memories WHERE id = ?').get(mem.id); + if (!existing) saveMemory(mem); + } + for (const s of data.settings) { + saveSetting(s.key, s.value); + } + }); + txn(); + + return { imported, skipped }; +} + +// ─── Close ─── + +export function closeDatabase(): void { + if (db) { + db.close(); + db = null; + } +} diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 1167e24..4d91c65 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -7,6 +7,16 @@ import * as fs from 'fs'; import * as path from 'path'; import { mainWindow } from './main.js'; import { showNotification } from './utils.js'; +import { + initDatabase, saveSession, getSession, getAllSessions, deleteSession, clearAllSessions, + saveMessage, getMessagesBySession, + saveMemory, getMemory, getAllMemories, getMemoriesByType, deleteMemory, clearAllMemories, searchMemoriesFTS, + saveSetting, getSetting, + saveToolCall, getToolCallsBySession, + saveTrace, getTracesBySession, + exportAllSessions, importSessions +} from './db/sqlite.js'; +import type { ExportData } from './db/sqlite.js'; /** 发送日志到渲染进程日志面板 */ function sendLog(level: 'info' | 'success' | 'warn' | 'error' | 'debug', message: string, detail?: string): void { @@ -251,4 +261,114 @@ export function setupIPC(): void { sendLog('info', `🔧 cmd:kill`, killed ? '已终止' : '无正在运行的工具命令'); return { killed }; }); + + // ══════════════════════════════════════════════ + // SQLite 数据库 IPC Handlers (v4.0) + // ══════════════════════════════════════════════ + + // 初始化数据库 + initDatabase(); + sendLog('success', '📦 SQLite 数据库已初始化'); + + // Sessions + ipcMain.handle('db:saveSession', (_, session) => { + try { return { success: true, id: saveSession(session) }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getSession', (_, id) => { + try { return getSession(id); } + catch { return null; } + }); + ipcMain.handle('db:getAllSessions', () => { + try { return getAllSessions(); } + catch { return []; } + }); + ipcMain.handle('db:deleteSession', (_, id) => { + try { deleteSession(id); return { success: true }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:clearAllSessions', () => { + try { clearAllSessions(); return { success: true }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + + // Messages + ipcMain.handle('db:saveMessage', (_, msg) => { + try { return { success: true, id: saveMessage(msg) }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getMessages', (_, sessionId) => { + try { return getMessagesBySession(sessionId); } + catch { return []; } + }); + + // Memories + ipcMain.handle('db:saveMemory', (_, entry) => { + try { return { success: true, id: saveMemory(entry) }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getMemory', (_, id) => { + try { return getMemory(id); } + catch { return null; } + }); + ipcMain.handle('db:getAllMemories', () => { + try { return getAllMemories(); } + catch { return []; } + }); + ipcMain.handle('db:getMemoriesByType', (_, type) => { + try { return getMemoriesByType(type); } + catch { return []; } + }); + ipcMain.handle('db:deleteMemory', (_, id) => { + try { deleteMemory(id); return { success: true }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:clearAllMemories', () => { + try { clearAllMemories(); return { success: true }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:searchMemories', (_, query: string, limit?: number) => { + try { return searchMemoriesFTS(query, limit ?? 10); } + catch { return []; } + }); + + // Settings + ipcMain.handle('db:saveSetting', (_, key: string, value: unknown) => { + try { saveSetting(key, value); return { success: true }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getSetting', (_, key: string, defaultValue?: unknown) => { + try { return getSetting(key, defaultValue ?? null); } + catch { return defaultValue ?? null; } + }); + + // Tool Calls + ipcMain.handle('db:saveToolCall', (_, tc) => { + try { return { success: true, id: saveToolCall(tc) }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getToolCalls', (_, sessionId) => { + try { return getToolCallsBySession(sessionId); } + catch { return []; } + }); + + // Traces + ipcMain.handle('db:saveTrace', (_, trace) => { + try { return { success: true, id: saveTrace(trace) }; } + catch (err) { return { success: false, error: (err as Error).message }; } + }); + ipcMain.handle('db:getTraces', (_, sessionId) => { + try { return getTracesBySession(sessionId); } + catch { return []; } + }); + + // Export/Import + ipcMain.handle('db:exportSessions', () => { + try { return exportAllSessions(); } + catch (err) { sendLog('error', '导出失败', (err as Error).message); return null; } + }); + ipcMain.handle('db:importSessions', (_, data: ExportData) => { + try { return importSessions(data); } + catch (err) { sendLog('error', '导入失败', (err as Error).message); return { imported: 0, skipped: 0 }; } + }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index 62e77b5..6c21a93 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -43,6 +43,30 @@ contextBridge.exposeInMainWorld('metonaDesktop', { onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => { ipcRenderer.on('main:log', (_: unknown, data: { level: string; message: string; detail?: string }) => callback(data)); }, + db: { + saveSession: (session: unknown) => ipcRenderer.invoke('db:saveSession', session), + getSession: (id: string) => ipcRenderer.invoke('db:getSession', id), + getAllSessions: () => ipcRenderer.invoke('db:getAllSessions'), + deleteSession: (id: string) => ipcRenderer.invoke('db:deleteSession', id), + clearAllSessions: () => ipcRenderer.invoke('db:clearAllSessions'), + saveMessage: (msg: unknown) => ipcRenderer.invoke('db:saveMessage', msg), + getMessages: (sessionId: string) => ipcRenderer.invoke('db:getMessages', sessionId), + saveMemory: (entry: unknown) => ipcRenderer.invoke('db:saveMemory', entry), + getMemory: (id: string) => ipcRenderer.invoke('db:getMemory', id), + getAllMemories: () => ipcRenderer.invoke('db:getAllMemories'), + getMemoriesByType: (type: string) => ipcRenderer.invoke('db:getMemoriesByType', type), + deleteMemory: (id: string) => ipcRenderer.invoke('db:deleteMemory', id), + clearAllMemories: () => ipcRenderer.invoke('db:clearAllMemories'), + searchMemories: (query: string, limit?: number) => ipcRenderer.invoke('db:searchMemories', query, limit), + saveSetting: (key: string, value: unknown) => ipcRenderer.invoke('db:saveSetting', key, value), + getSetting: (key: string, defaultValue?: unknown) => ipcRenderer.invoke('db:getSetting', key, defaultValue), + saveToolCall: (tc: unknown) => ipcRenderer.invoke('db:saveToolCall', tc), + getToolCalls: (sessionId: string) => ipcRenderer.invoke('db:getToolCalls', sessionId), + saveTrace: (trace: unknown) => ipcRenderer.invoke('db:saveTrace', trace), + getTraces: (sessionId: string) => ipcRenderer.invoke('db:getTraces', sessionId), + exportSessions: () => ipcRenderer.invoke('db:exportSessions'), + importSessions: (data: unknown) => ipcRenderer.invoke('db:importSessions', data), + }, workspace: { getDir: () => ipcRenderer.invoke('workspace:getDir'), setDir: (dir: string) => ipcRenderer.invoke('workspace:setDir', dir), diff --git a/src/renderer/components/chat-area.ts b/src/renderer/components/chat-area.ts index d5e449b..fdc3654 100644 --- a/src/renderer/components/chat-area.ts +++ b/src/renderer/components/chat-area.ts @@ -188,11 +188,15 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void { function renderToolCallCard(tc: ToolCallRecord): string { const icons: Record = { read_file: '📄', write_file: '✏️', list_directory: '📁', - search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻' + search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻', + web_search: '🔍', web_fetch: '🌐', git: '🔖', + memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖' }; const names: Record = { read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录', - search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', run_command: '执行命令' + search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', run_command: '执行命令', + web_search: '联网搜索', web_fetch: '网页抓取', git: 'Git 操作', + memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话' }; const statusLabels: Record = { pending: '⏳ 等待确认', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消' diff --git a/src/renderer/db/chat-db.ts b/src/renderer/db/chat-db.ts index b25081e..70ecdc0 100644 --- a/src/renderer/db/chat-db.ts +++ b/src/renderer/db/chat-db.ts @@ -1,14 +1,24 @@ /** - * ChatDB - IndexedDB 封装层 - * v2: 新增 memories 存储(Agent 记忆系统) + * ChatDB - 存储兼容层 + * v4.0: 桌面端走 IPC → SQLite,Web 端仍用 IndexedDB */ import type { ChatSession, MemoryEntry } from '../types.js'; +/** 检查是否在桌面环境 */ +function isDesktop(): boolean { + return !!(window as any).metonaDesktop?.isDesktop; +} + +/** 桌面端 DB 桥接 */ +function dbBridge() { + return (window as any).metonaDesktop?.db; +} + export class ChatDB { private dbName: string; private version: number; - private db: IDBDatabase | null = null; + private idb: IDBDatabase | null = null; constructor(dbName = 'metona-ollama', version = 2) { this.dbName = dbName; @@ -16,15 +26,22 @@ export class ChatDB { } async init(): Promise { + if (isDesktop()) { + // 桌面端:SQLite 已在主进程初始化,这里不需要 IndexedDB + // 但为了数据迁移,仍打开 IndexedDB(只读) + await this._openIDB(); + return; + } + // Web 端:使用 IndexedDB + await this._openIDB(); + } + + /** 打开 IndexedDB(用于迁移或 Web 端 fallback) */ + private async _openIDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); - request.onerror = () => { - reject(request.error); - }; - request.onsuccess = () => { - this.db = request.result; - resolve(); - }; + request.onerror = () => reject(request.error); + request.onsuccess = () => { this.idb = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains('sessions')) { @@ -46,172 +63,379 @@ export class ChatDB { }); } - private _ensureDB(): void { - if (!this.db) throw new Error('数据库未初始化,请先调用 init()'); - } - - private _tx(storeName: string, mode: IDBTransactionMode = 'readonly'): IDBObjectStore { - this._ensureDB(); - const tx = this.db!.transaction(storeName, mode); - return tx.objectStore(storeName); - } + // ── Sessions ── async saveSession(session: ChatSession): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('sessions', 'readwrite'); - const request = store.put(session); - request.onsuccess = () => resolve(session.id); - request.onerror = () => reject(request.error); - }); + if (isDesktop()) { + const row = { + id: session.id, + title: session.title, + model: session.model, + system_prompt: null, + parent_id: null, + status: 'active', + created_at: session.createdAt, + updated_at: session.updatedAt + }; + await dbBridge().saveSession(row); + // 同步保存消息 + for (const msg of session.messages) { + const msgRow = { + id: `${session.id}_${msg.timestamp}_${msg.role}`, + session_id: session.id, + role: msg.role, + content: msg.content || null, + thinking: msg.think || null, + images: msg.images?.length ? JSON.stringify(msg.images) : null, + tool_calls: msg.toolCalls?.length ? JSON.stringify(msg.toolCalls) : null, + tool_name: null, + eval_count: msg.eval_count || null, + total_duration: msg.total_duration || null, + created_at: msg.timestamp + }; + await dbBridge().saveMessage(msgRow); + } + return session.id; + } + return this._idbSaveSession(session); } async getSession(id: string): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('sessions'); - const request = store.get(id); - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); - }); + if (isDesktop()) { + const row = await dbBridge().getSession(id); + if (!row) return null; + const msgRows = await dbBridge().getMessages(id); + const messages = msgRows.map((r: any) => ({ + role: r.role, + content: r.content || '', + timestamp: r.created_at, + think: r.thinking || undefined, + images: r.images ? JSON.parse(r.images) : undefined, + eval_count: r.eval_count || undefined, + total_duration: r.total_duration || undefined, + toolCalls: r.tool_calls ? JSON.parse(r.tool_calls) : undefined + })); + return { + id: row.id, + title: row.title, + model: row.model, + messages, + createdAt: row.created_at, + updatedAt: row.updated_at + }; + } + return this._idbGetSession(id); } async getAllSessions(): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('sessions'); - const request = store.getAll(); - request.onsuccess = () => resolve(request.result || []); - request.onerror = () => reject(request.error); - }); + if (isDesktop()) { + const rows = await dbBridge().getAllSessions(); + const sessions: ChatSession[] = []; + for (const row of rows) { + const session = await this.getSession(row.id); + if (session) sessions.push(session); + } + return sessions; + } + return this._idbGetAllSessions(); } async deleteSession(id: string): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('sessions', 'readwrite'); - const request = store.delete(id); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + if (isDesktop()) { + await dbBridge().deleteSession(id); + return; + } + return this._idbDeleteSession(id); } async clearAll(): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('sessions', 'readwrite'); - const request = store.clear(); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + if (isDesktop()) { + await dbBridge().clearAllSessions(); + return; + } + return this._idbClearAll(); } async importSessions(sessions: ChatSession[]): Promise<{ imported: number; skipped: number }> { - return new Promise((resolve, reject) => { - const tx = this.db!.transaction('sessions', 'readwrite'); - const store = tx.objectStore('sessions'); - let imported = 0; - let skipped = 0; + if (isDesktop()) { + const data = { + sessions: sessions.map(s => ({ + id: s.id, title: s.title, model: s.model, system_prompt: null, + parent_id: null, status: 'active', created_at: s.createdAt, updated_at: s.updatedAt + })), + messages: sessions.flatMap(s => s.messages.map(m => ({ + id: `${s.id}_${m.timestamp}_${m.role}`, + session_id: s.id, role: m.role, content: m.content || null, + thinking: m.think || null, + images: m.images?.length ? JSON.stringify(m.images) : null, + tool_calls: m.toolCalls?.length ? JSON.stringify(m.toolCalls) : null, + tool_name: null, eval_count: m.eval_count || null, + total_duration: m.total_duration || null, created_at: m.timestamp + }))), + memories: [], + settings: [], + exportedAt: Date.now() + }; + return dbBridge().importSessions(data); + } + return this._idbImportSessions(sessions); + } + async getSessionsByTimeRange(startTime: number, endTime: number): Promise { + const all = await this.getAllSessions(); + return all.filter(s => s.updatedAt >= startTime && s.updatedAt <= endTime); + } + + // ── Settings ── + + async saveSetting(key: string, value: unknown): Promise { + if (isDesktop()) { + await dbBridge().saveSetting(key, value); + return; + } + return this._idbSaveSetting(key, value); + } + + async getSetting(key: string, defaultValue: T | null = null): Promise { + if (isDesktop()) { + return dbBridge().getSetting(key, defaultValue); + } + return this._idbGetSetting(key, defaultValue); + } + + // ── Memories ── + + async saveMemory(entry: MemoryEntry): Promise { + if (isDesktop()) { + const row = { + id: entry.id, + type: entry.type, + content: entry.content, + importance: entry.importance, + tags: entry.tags?.length ? JSON.stringify(entry.tags) : null, + source: entry.source || null, + session_id: entry.sessionId || null, + use_count: entry.useCount, + embedding: entry.embedding ? JSON.stringify(entry.embedding) : null, + created_at: entry.createdAt, + updated_at: entry.updatedAt, + last_used_at: entry.lastUsedAt + }; + await dbBridge().saveMemory(row); + return entry.id; + } + return this._idbSaveMemory(entry); + } + + async getMemory(id: string): Promise { + if (isDesktop()) { + const row = await dbBridge().getMemory(id); + return row ? this._rowToMemory(row) : null; + } + return this._idbGetMemory(id); + } + + async getAllMemories(): Promise { + if (isDesktop()) { + const rows = await dbBridge().getAllMemories(); + return rows.map((r: any) => this._rowToMemory(r)); + } + return this._idbGetAllMemories(); + } + + async getMemoriesByType(type: string): Promise { + if (isDesktop()) { + const rows = await dbBridge().getMemoriesByType(type); + return rows.map((r: any) => this._rowToMemory(r)); + } + return this._idbGetMemoriesByType(type); + } + + async deleteMemory(id: string): Promise { + if (isDesktop()) { + await dbBridge().deleteMemory(id); + return; + } + return this._idbDeleteMemory(id); + } + + async clearAllMemories(): Promise { + if (isDesktop()) { + await dbBridge().clearAllMemories(); + return; + } + return this._idbClearAllMemories(); + } + + // ── Helpers ── + + private _rowToMemory(row: any): MemoryEntry { + return { + id: row.id, + type: row.type, + content: row.content, + importance: row.importance, + tags: row.tags ? JSON.parse(row.tags) : [], + source: row.source || undefined, + sessionId: row.session_id || undefined, + useCount: row.use_count, + embedding: row.embedding ? JSON.parse(row.embedding) : undefined, + createdAt: row.created_at, + updatedAt: row.updated_at, + lastUsedAt: row.last_used_at + }; + } + + // ═══════════════════════════════════════════ + // IndexedDB fallback methods (Web 端兼容) + // ═══════════════════════════════════════════ + + private _ensureIDB(): void { + if (!this.idb) throw new Error('IndexedDB 未初始化'); + } + + private _idbTx(storeName: string, mode: IDBTransactionMode = 'readonly'): IDBObjectStore { + this._ensureIDB(); + return this.idb!.transaction(storeName, mode).objectStore(storeName); + } + + private async _idbSaveSession(session: ChatSession): Promise { + return new Promise((resolve, reject) => { + const store = this._idbTx('sessions', 'readwrite'); + const req = store.put(session); + req.onsuccess = () => resolve(session.id); + req.onerror = () => reject(req.error); + }); + } + + private async _idbGetSession(id: string): Promise { + return new Promise((resolve, reject) => { + const store = this._idbTx('sessions'); + const req = store.get(id); + req.onsuccess = () => resolve(req.result || null); + req.onerror = () => reject(req.error); + }); + } + + private async _idbGetAllSessions(): Promise { + return new Promise((resolve, reject) => { + const store = this._idbTx('sessions'); + const req = store.getAll(); + req.onsuccess = () => resolve(req.result || []); + req.onerror = () => reject(req.error); + }); + } + + private async _idbDeleteSession(id: string): Promise { + return new Promise((resolve, reject) => { + const store = this._idbTx('sessions', 'readwrite'); + const req = store.delete(id); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + + private async _idbClearAll(): Promise { + return new Promise((resolve, reject) => { + const store = this._idbTx('sessions', 'readwrite'); + const req = store.clear(); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + + private async _idbImportSessions(sessions: ChatSession[]): Promise<{ imported: number; skipped: number }> { + return new Promise((resolve, reject) => { + const tx = this.idb!.transaction('sessions', 'readwrite'); + const store = tx.objectStore('sessions'); + let imported = 0; let skipped = 0; tx.oncomplete = () => resolve({ imported, skipped }); tx.onerror = () => reject(tx.error); - for (const session of sessions) { - if (!session.id || !Array.isArray(session.messages)) { - skipped++; - continue; - } - const getRequest = store.get(session.id); - getRequest.onsuccess = () => { - if (getRequest.result) { - skipped++; - } else { - store.put(session); - imported++; - } + if (!session.id || !Array.isArray(session.messages)) { skipped++; continue; } + const getReq = store.get(session.id); + getReq.onsuccess = () => { + if (getReq.result) { skipped++; } else { store.put(session); imported++; } }; } }); } - async getSessionsByTimeRange(startTime: number, endTime: number): Promise { + private async _idbSaveSetting(key: string, value: unknown): Promise { return new Promise((resolve, reject) => { - const store = this._tx('sessions'); - const index = store.index('updatedAt'); - const range = IDBKeyRange.bound(startTime, endTime); - const request = index.getAll(range); - request.onsuccess = () => resolve(request.result || []); - request.onerror = () => reject(request.error); + const store = this._idbTx('settings', 'readwrite'); + const req = store.put({ key, value }); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); }); } - async saveSetting(key: string, value: unknown): Promise { + private async _idbGetSetting(key: string, defaultValue: T | null = null): Promise { return new Promise((resolve, reject) => { - const store = this._tx('settings', 'readwrite'); - const request = store.put({ key, value }); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + const store = this._idbTx('settings'); + const req = store.get(key); + req.onsuccess = () => resolve(req.result ? req.result.value : defaultValue); + req.onerror = () => reject(req.error); }); } - async getSetting(key: string, defaultValue: T | null = null): Promise { + private async _idbSaveMemory(entry: MemoryEntry): Promise { return new Promise((resolve, reject) => { - const store = this._tx('settings'); - const request = store.get(key); - request.onsuccess = () => resolve(request.result ? request.result.value : defaultValue); - request.onerror = () => reject(request.error); + const store = this._idbTx('memories', 'readwrite'); + const req = store.put(entry); + req.onsuccess = () => resolve(entry.id); + req.onerror = () => reject(req.error); }); } - // ── Agent 记忆系统 CRUD ── - - async saveMemory(entry: MemoryEntry): Promise { + private async _idbGetMemory(id: string): Promise { return new Promise((resolve, reject) => { - const store = this._tx('memories', 'readwrite'); - const request = store.put(entry); - request.onsuccess = () => resolve(entry.id); - request.onerror = () => reject(request.error); + const store = this._idbTx('memories'); + const req = store.get(id); + req.onsuccess = () => resolve(req.result || null); + req.onerror = () => reject(req.error); }); } - async getMemory(id: string): Promise { + private async _idbGetAllMemories(): Promise { return new Promise((resolve, reject) => { - const store = this._tx('memories'); - const request = store.get(id); - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); + const store = this._idbTx('memories'); + const req = store.getAll(); + req.onsuccess = () => resolve(req.result || []); + req.onerror = () => reject(req.error); }); } - async getAllMemories(): Promise { + private async _idbGetMemoriesByType(type: string): Promise { return new Promise((resolve, reject) => { - const store = this._tx('memories'); - const request = store.getAll(); - request.onsuccess = () => resolve(request.result || []); - request.onerror = () => reject(request.error); - }); - } - - async getMemoriesByType(type: string): Promise { - return new Promise((resolve, reject) => { - const store = this._tx('memories'); + const store = this._idbTx('memories'); const index = store.index('type'); - const request = index.getAll(type); - request.onsuccess = () => resolve(request.result || []); - request.onerror = () => reject(request.error); + const req = index.getAll(type); + req.onsuccess = () => resolve(req.result || []); + req.onerror = () => reject(req.error); }); } - async deleteMemory(id: string): Promise { + private async _idbDeleteMemory(id: string): Promise { return new Promise((resolve, reject) => { - const store = this._tx('memories', 'readwrite'); - const request = store.delete(id); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + const store = this._idbTx('memories', 'readwrite'); + const req = store.delete(id); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); }); } - async clearAllMemories(): Promise { + private async _idbClearAllMemories(): Promise { return new Promise((resolve, reject) => { - const store = this._tx('memories', 'readwrite'); - const request = store.clear(); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + const store = this._idbTx('memories', 'readwrite'); + const req = store.clear(); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); }); } + + /** 获取原始 IndexedDB 实例(用于数据迁移) */ + getRawIDB(): IDBDatabase | null { + return this.idb; + } } diff --git a/src/renderer/index.html b/src/renderer/index.html index b2ce17c..1bdb68e 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -27,7 +27,7 @@
Metona Ollama - v3.3.4 + v4.0.0