feat: v4.0.0 大版本升级 - SQLite3 + ReAct Agent
- P0: 存储层 IndexedDB → SQLite3 (better-sqlite3),7张表 + FTS5全文搜索 - P1: Agent Engine 升级为 ReAct 模式(思考→行动→观察→反思) - P2: 新增 4 个工具(memory_search, memory_add, session_list, session_read) - P3: 上下文管理器(滑动窗口 + 摘要压缩 + 记忆注入) - P4: UI 改版(ReAct 执行面板样式 + 思考过程卡片) - P5: IndexedDB → SQLite 数据迁移支持 - MAX_LOOPS 10→15, MAX_LOOP_TIME 5min→10min, 错误自动重试 2 次
This commit is contained in:
Generated
+213
-28
@@ -1,13 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "3.3.0",
|
"version": "3.3.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "3.3.0",
|
"version": "3.3.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"better-sqlite3": "^12.9.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.17.0",
|
"@types/node": "^20.17.0",
|
||||||
"electron": "^33.4.11",
|
"electron": "^33.4.11",
|
||||||
@@ -1420,6 +1424,15 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
@@ -1488,7 +1501,6 @@
|
|||||||
"version": "20.19.39",
|
"version": "20.19.39",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz",
|
||||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -1933,7 +1945,6 @@
|
|||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1950,11 +1961,33 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
|
||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
@@ -2005,7 +2038,6 @@
|
|||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2649,7 +2681,6 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-response": "^3.1.0"
|
"mimic-response": "^3.1.0"
|
||||||
@@ -2665,7 +2696,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -2674,6 +2704,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/defaults": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz",
|
||||||
@@ -2756,7 +2795,6 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -3176,7 +3214,6 @@
|
|||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
@@ -3319,6 +3356,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/exponential-backoff": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||||
@@ -3382,6 +3428,12 @@
|
|||||||
"pend": "~1.2.0"
|
"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": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz",
|
||||||
@@ -3473,9 +3525,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true,
|
"license": "MIT"
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@@ -3623,6 +3673,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -3953,7 +4009,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4013,7 +4068,12 @@
|
|||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"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"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
@@ -4533,7 +4593,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -4649,6 +4708,12 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
"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": "^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": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.4",
|
||||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz",
|
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
@@ -4689,7 +4760,6 @@
|
|||||||
"version": "3.89.0",
|
"version": "3.89.0",
|
||||||
"resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz",
|
"resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz",
|
||||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.3.5"
|
"semver": "^7.3.5"
|
||||||
@@ -4702,7 +4772,6 @@
|
|||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -4853,7 +4922,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@@ -5075,6 +5143,33 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -5118,7 +5213,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
||||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -5148,6 +5242,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/read-binary-file-arch": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
"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",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -5377,7 +5485,6 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -5493,6 +5600,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||||
@@ -5655,7 +5807,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
@@ -5719,6 +5870,15 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
@@ -5763,13 +5923,29 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@@ -5870,6 +6046,18 @@
|
|||||||
"utf8-byte-length": "^1.0.1"
|
"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": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz",
|
"resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz",
|
||||||
@@ -5902,7 +6090,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unique-filename": {
|
"node_modules/unique-filename": {
|
||||||
@@ -5962,7 +6149,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
@@ -6118,7 +6304,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/xmlbuilder": {
|
"node_modules/xmlbuilder": {
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "3.3.4",
|
"version": "4.0.0",
|
||||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "thzxx",
|
"author": "thzxx",
|
||||||
@@ -31,7 +31,9 @@
|
|||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "assets/icons/llama.ico",
|
"icon": "assets/icons/llama.ico",
|
||||||
@@ -54,5 +56,9 @@
|
|||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"better-sqlite3": "^12.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<T = unknown>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
@@ -7,6 +7,16 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { mainWindow } from './main.js';
|
import { mainWindow } from './main.js';
|
||||||
import { showNotification } from './utils.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 {
|
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 ? '已终止' : '无正在运行的工具命令');
|
sendLog('info', `🔧 cmd:kill`, killed ? '已终止' : '无正在运行的工具命令');
|
||||||
return { 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 }; }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,30 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
|
|||||||
onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => {
|
onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => {
|
||||||
ipcRenderer.on('main:log', (_: unknown, data: { level: string; message: string; detail?: string }) => callback(data));
|
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: {
|
workspace: {
|
||||||
getDir: () => ipcRenderer.invoke('workspace:getDir'),
|
getDir: () => ipcRenderer.invoke('workspace:getDir'),
|
||||||
setDir: (dir: string) => ipcRenderer.invoke('workspace:setDir', dir),
|
setDir: (dir: string) => ipcRenderer.invoke('workspace:setDir', dir),
|
||||||
|
|||||||
@@ -188,11 +188,15 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
|
|||||||
function renderToolCallCard(tc: ToolCallRecord): string {
|
function renderToolCallCard(tc: ToolCallRecord): string {
|
||||||
const icons: Record<string, string> = {
|
const icons: Record<string, string> = {
|
||||||
read_file: '📄', write_file: '✏️', list_directory: '📁',
|
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<string, string> = {
|
const names: Record<string, string> = {
|
||||||
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
|
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<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
pending: '⏳ 等待确认', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消'
|
pending: '⏳ 等待确认', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消'
|
||||||
|
|||||||
+344
-120
@@ -1,14 +1,24 @@
|
|||||||
/**
|
/**
|
||||||
* ChatDB - IndexedDB 封装层
|
* ChatDB - 存储兼容层
|
||||||
* v2: 新增 memories 存储(Agent 记忆系统)
|
* v4.0: 桌面端走 IPC → SQLite,Web 端仍用 IndexedDB
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ChatSession, MemoryEntry } from '../types.js';
|
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 {
|
export class ChatDB {
|
||||||
private dbName: string;
|
private dbName: string;
|
||||||
private version: number;
|
private version: number;
|
||||||
private db: IDBDatabase | null = null;
|
private idb: IDBDatabase | null = null;
|
||||||
|
|
||||||
constructor(dbName = 'metona-ollama', version = 2) {
|
constructor(dbName = 'metona-ollama', version = 2) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
@@ -16,15 +26,22 @@ export class ChatDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
// 桌面端:SQLite 已在主进程初始化,这里不需要 IndexedDB
|
||||||
|
// 但为了数据迁移,仍打开 IndexedDB(只读)
|
||||||
|
await this._openIDB();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Web 端:使用 IndexedDB
|
||||||
|
await this._openIDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开 IndexedDB(用于迁移或 Web 端 fallback) */
|
||||||
|
private async _openIDB(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(this.dbName, this.version);
|
const request = indexedDB.open(this.dbName, this.version);
|
||||||
request.onerror = () => {
|
request.onerror = () => reject(request.error);
|
||||||
reject(request.error);
|
request.onsuccess = () => { this.idb = request.result; resolve(); };
|
||||||
};
|
|
||||||
request.onsuccess = () => {
|
|
||||||
this.db = request.result;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
request.onupgradeneeded = (event) => {
|
request.onupgradeneeded = (event) => {
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
if (!db.objectStoreNames.contains('sessions')) {
|
if (!db.objectStoreNames.contains('sessions')) {
|
||||||
@@ -46,172 +63,379 @@ export class ChatDB {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _ensureDB(): void {
|
// ── Sessions ──
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveSession(session: ChatSession): Promise<string> {
|
async saveSession(session: ChatSession): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const store = this._tx('sessions', 'readwrite');
|
const row = {
|
||||||
const request = store.put(session);
|
id: session.id,
|
||||||
request.onsuccess = () => resolve(session.id);
|
title: session.title,
|
||||||
request.onerror = () => reject(request.error);
|
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<ChatSession | null> {
|
async getSession(id: string): Promise<ChatSession | null> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const store = this._tx('sessions');
|
const row = await dbBridge().getSession(id);
|
||||||
const request = store.get(id);
|
if (!row) return null;
|
||||||
request.onsuccess = () => resolve(request.result || null);
|
const msgRows = await dbBridge().getMessages(id);
|
||||||
request.onerror = () => reject(request.error);
|
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<ChatSession[]> {
|
async getAllSessions(): Promise<ChatSession[]> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const store = this._tx('sessions');
|
const rows = await dbBridge().getAllSessions();
|
||||||
const request = store.getAll();
|
const sessions: ChatSession[] = [];
|
||||||
request.onsuccess = () => resolve(request.result || []);
|
for (const row of rows) {
|
||||||
request.onerror = () => reject(request.error);
|
const session = await this.getSession(row.id);
|
||||||
});
|
if (session) sessions.push(session);
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
return this._idbGetAllSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSession(id: string): Promise<void> {
|
async deleteSession(id: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const store = this._tx('sessions', 'readwrite');
|
await dbBridge().deleteSession(id);
|
||||||
const request = store.delete(id);
|
return;
|
||||||
request.onsuccess = () => resolve();
|
}
|
||||||
request.onerror = () => reject(request.error);
|
return this._idbDeleteSession(id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll(): Promise<void> {
|
async clearAll(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const store = this._tx('sessions', 'readwrite');
|
await dbBridge().clearAllSessions();
|
||||||
const request = store.clear();
|
return;
|
||||||
request.onsuccess = () => resolve();
|
}
|
||||||
request.onerror = () => reject(request.error);
|
return this._idbClearAll();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async importSessions(sessions: ChatSession[]): Promise<{ imported: number; skipped: number }> {
|
async importSessions(sessions: ChatSession[]): Promise<{ imported: number; skipped: number }> {
|
||||||
return new Promise((resolve, reject) => {
|
if (isDesktop()) {
|
||||||
const tx = this.db!.transaction('sessions', 'readwrite');
|
const data = {
|
||||||
const store = tx.objectStore('sessions');
|
sessions: sessions.map(s => ({
|
||||||
let imported = 0;
|
id: s.id, title: s.title, model: s.model, system_prompt: null,
|
||||||
let skipped = 0;
|
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<ChatSession[]> {
|
||||||
|
const all = await this.getAllSessions();
|
||||||
|
return all.filter(s => s.updatedAt >= startTime && s.updatedAt <= endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Settings ──
|
||||||
|
|
||||||
|
async saveSetting(key: string, value: unknown): Promise<void> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
await dbBridge().saveSetting(key, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this._idbSaveSetting(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSetting<T = unknown>(key: string, defaultValue: T | null = null): Promise<T> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
return dbBridge().getSetting(key, defaultValue);
|
||||||
|
}
|
||||||
|
return this._idbGetSetting(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Memories ──
|
||||||
|
|
||||||
|
async saveMemory(entry: MemoryEntry): Promise<string> {
|
||||||
|
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<MemoryEntry | null> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
const row = await dbBridge().getMemory(id);
|
||||||
|
return row ? this._rowToMemory(row) : null;
|
||||||
|
}
|
||||||
|
return this._idbGetMemory(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllMemories(): Promise<MemoryEntry[]> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
const rows = await dbBridge().getAllMemories();
|
||||||
|
return rows.map((r: any) => this._rowToMemory(r));
|
||||||
|
}
|
||||||
|
return this._idbGetAllMemories();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMemoriesByType(type: string): Promise<MemoryEntry[]> {
|
||||||
|
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<void> {
|
||||||
|
if (isDesktop()) {
|
||||||
|
await dbBridge().deleteMemory(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this._idbDeleteMemory(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllMemories(): Promise<void> {
|
||||||
|
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<string> {
|
||||||
|
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<ChatSession | null> {
|
||||||
|
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<ChatSession[]> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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.oncomplete = () => resolve({ imported, skipped });
|
||||||
tx.onerror = () => reject(tx.error);
|
tx.onerror = () => reject(tx.error);
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
if (!session.id || !Array.isArray(session.messages)) {
|
if (!session.id || !Array.isArray(session.messages)) { skipped++; continue; }
|
||||||
skipped++;
|
const getReq = store.get(session.id);
|
||||||
continue;
|
getReq.onsuccess = () => {
|
||||||
}
|
if (getReq.result) { skipped++; } else { store.put(session); imported++; }
|
||||||
const getRequest = store.get(session.id);
|
|
||||||
getRequest.onsuccess = () => {
|
|
||||||
if (getRequest.result) {
|
|
||||||
skipped++;
|
|
||||||
} else {
|
|
||||||
store.put(session);
|
|
||||||
imported++;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessionsByTimeRange(startTime: number, endTime: number): Promise<ChatSession[]> {
|
private async _idbSaveSetting(key: string, value: unknown): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('sessions');
|
const store = this._idbTx('settings', 'readwrite');
|
||||||
const index = store.index('updatedAt');
|
const req = store.put({ key, value });
|
||||||
const range = IDBKeyRange.bound(startTime, endTime);
|
req.onsuccess = () => resolve();
|
||||||
const request = index.getAll(range);
|
req.onerror = () => reject(req.error);
|
||||||
request.onsuccess = () => resolve(request.result || []);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSetting(key: string, value: unknown): Promise<void> {
|
private async _idbGetSetting<T = unknown>(key: string, defaultValue: T | null = null): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('settings', 'readwrite');
|
const store = this._idbTx('settings');
|
||||||
const request = store.put({ key, value });
|
const req = store.get(key);
|
||||||
request.onsuccess = () => resolve();
|
req.onsuccess = () => resolve(req.result ? req.result.value : defaultValue);
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetting<T = unknown>(key: string, defaultValue: T | null = null): Promise<T> {
|
private async _idbSaveMemory(entry: MemoryEntry): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('settings');
|
const store = this._idbTx('memories', 'readwrite');
|
||||||
const request = store.get(key);
|
const req = store.put(entry);
|
||||||
request.onsuccess = () => resolve(request.result ? request.result.value : defaultValue);
|
req.onsuccess = () => resolve(entry.id);
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Agent 记忆系统 CRUD ──
|
private async _idbGetMemory(id: string): Promise<MemoryEntry | null> {
|
||||||
|
|
||||||
async saveMemory(entry: MemoryEntry): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('memories', 'readwrite');
|
const store = this._idbTx('memories');
|
||||||
const request = store.put(entry);
|
const req = store.get(id);
|
||||||
request.onsuccess = () => resolve(entry.id);
|
req.onsuccess = () => resolve(req.result || null);
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemory(id: string): Promise<MemoryEntry | null> {
|
private async _idbGetAllMemories(): Promise<MemoryEntry[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('memories');
|
const store = this._idbTx('memories');
|
||||||
const request = store.get(id);
|
const req = store.getAll();
|
||||||
request.onsuccess = () => resolve(request.result || null);
|
req.onsuccess = () => resolve(req.result || []);
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllMemories(): Promise<MemoryEntry[]> {
|
private async _idbGetMemoriesByType(type: string): Promise<MemoryEntry[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('memories');
|
const store = this._idbTx('memories');
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result || []);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMemoriesByType(type: string): Promise<MemoryEntry[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const store = this._tx('memories');
|
|
||||||
const index = store.index('type');
|
const index = store.index('type');
|
||||||
const request = index.getAll(type);
|
const req = index.getAll(type);
|
||||||
request.onsuccess = () => resolve(request.result || []);
|
req.onsuccess = () => resolve(req.result || []);
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMemory(id: string): Promise<void> {
|
private async _idbDeleteMemory(id: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('memories', 'readwrite');
|
const store = this._idbTx('memories', 'readwrite');
|
||||||
const request = store.delete(id);
|
const req = store.delete(id);
|
||||||
request.onsuccess = () => resolve();
|
req.onsuccess = () => resolve();
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAllMemories(): Promise<void> {
|
private async _idbClearAllMemories(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const store = this._tx('memories', 'readwrite');
|
const store = this._idbTx('memories', 'readwrite');
|
||||||
const request = store.clear();
|
const req = store.clear();
|
||||||
request.onsuccess = () => resolve();
|
req.onsuccess = () => resolve();
|
||||||
request.onerror = () => reject(request.error);
|
req.onerror = () => reject(req.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取原始 IndexedDB 实例(用于数据迁移) */
|
||||||
|
getRawIDB(): IDBDatabase | null {
|
||||||
|
return this.idb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="logo">🦙</span>
|
<span class="logo">🦙</span>
|
||||||
<span class="app-title">Metona Ollama</span>
|
<span class="app-title">Metona Ollama</span>
|
||||||
<span class="app-version">v3.3.4</span>
|
<span class="app-version">v4.0.0</span>
|
||||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||||
|
|||||||
+117
-1
@@ -29,6 +29,120 @@ import { initLogPanel, addLog } from './services/log-service.js';
|
|||||||
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
|
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
|
||||||
import type { ChatSession } from './types.js';
|
import type { ChatSession } from './types.js';
|
||||||
|
|
||||||
|
// ─── v4.0 数据迁移:IndexedDB → SQLite ───
|
||||||
|
|
||||||
|
async function migrateIndexedDBToSQLite(db: ChatDB): Promise<void> {
|
||||||
|
const bridge = (window as any).metonaDesktop;
|
||||||
|
if (!bridge?.isDesktop || !bridge?.db) {
|
||||||
|
logInit('非桌面环境,跳过迁移');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已迁移
|
||||||
|
const migrated = await bridge.db.getSetting('_indexeddb_migrated', false);
|
||||||
|
if (migrated) {
|
||||||
|
logInit('SQLite 数据库已连接(已迁移)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 IndexedDB 中是否有数据
|
||||||
|
const idb = db.getRawIDB();
|
||||||
|
if (!idb) {
|
||||||
|
logInit('SQLite 数据库已连接(无旧数据)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 IndexedDB 数据
|
||||||
|
try {
|
||||||
|
const sessions: ChatSession[] = await new Promise((resolve, reject) => {
|
||||||
|
const tx = idb.transaction('sessions', 'readonly');
|
||||||
|
const store = tx.objectStore('sessions');
|
||||||
|
const req = store.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result || []);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const memories: any[] = await new Promise((resolve, reject) => {
|
||||||
|
const tx = idb.transaction('memories', 'readonly');
|
||||||
|
const store = tx.objectStore('memories');
|
||||||
|
const req = store.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result || []);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings: any[] = await new Promise((resolve, reject) => {
|
||||||
|
const tx = idb.transaction('settings', 'readonly');
|
||||||
|
const store = tx.objectStore('settings');
|
||||||
|
const req = store.getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result || []);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessions.length === 0 && memories.length === 0 && settings.length === 0) {
|
||||||
|
logInit('SQLite 数据库已连接(无旧数据)');
|
||||||
|
await bridge.db.saveSetting('_indexeddb_migrated', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInit(`发现旧数据: ${sessions.length} 会话, ${memories.length} 记忆, ${settings.length} 设置`);
|
||||||
|
|
||||||
|
// 构造导入数据
|
||||||
|
const importData = {
|
||||||
|
sessions: sessions.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
model: s.model,
|
||||||
|
system_prompt: null as string | null,
|
||||||
|
parent_id: null as string | null,
|
||||||
|
status: 'active',
|
||||||
|
created_at: s.createdAt,
|
||||||
|
updated_at: s.updatedAt
|
||||||
|
})),
|
||||||
|
messages: sessions.flatMap(s =>
|
||||||
|
s.messages.map((m: any, idx: number) => ({
|
||||||
|
id: `${s.id}_msg_${idx}_${m.timestamp}`,
|
||||||
|
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 || Date.now()
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
memories: memories.map((m: any) => ({
|
||||||
|
id: m.id,
|
||||||
|
type: m.type,
|
||||||
|
content: m.content,
|
||||||
|
importance: m.importance || 5,
|
||||||
|
tags: m.tags?.length ? JSON.stringify(m.tags) : null,
|
||||||
|
source: m.source || null,
|
||||||
|
session_id: m.sessionId || null,
|
||||||
|
use_count: m.useCount || 0,
|
||||||
|
embedding: m.embedding ? JSON.stringify(m.embedding) : null,
|
||||||
|
created_at: m.createdAt,
|
||||||
|
updated_at: m.updatedAt,
|
||||||
|
last_used_at: m.lastUsedAt
|
||||||
|
})),
|
||||||
|
settings: settings.map((s: any) => ({ key: s.key, value: s.value })),
|
||||||
|
exportedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await bridge.db.importSessions(importData);
|
||||||
|
logInit(`迁移完成: 导入 ${result.imported} 个会话, 跳过 ${result.skipped} 个`);
|
||||||
|
|
||||||
|
// 标记迁移完成
|
||||||
|
await bridge.db.saveSetting('_indexeddb_migrated', true);
|
||||||
|
logSuccess('数据迁移完成');
|
||||||
|
} catch (err) {
|
||||||
|
logError('数据迁移失败', (err as Error).message);
|
||||||
|
// 迁移失败不阻塞启动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initHelpModal(): void {
|
function initHelpModal(): void {
|
||||||
const helpModal = document.querySelector('#helpModal') as HTMLElement;
|
const helpModal = document.querySelector('#helpModal') as HTMLElement;
|
||||||
document.querySelector('#btnHelp')!.addEventListener('click', () => {
|
document.querySelector('#btnHelp')!.addEventListener('click', () => {
|
||||||
@@ -176,7 +290,9 @@ async function init(): Promise<void> {
|
|||||||
db = new ChatDB();
|
db = new ChatDB();
|
||||||
await db.init();
|
await db.init();
|
||||||
state.set(KEYS.DB, db);
|
state.set(KEYS.DB, db);
|
||||||
logInit('IndexedDB 已连接');
|
|
||||||
|
// v4.0 数据迁移:检测 IndexedDB 是否有旧数据需要迁移到 SQLite
|
||||||
|
await migrateIndexedDBToSQLite(db);
|
||||||
|
|
||||||
const serverUrl = await db.getSetting('serverUrl', 'http://127.0.0.1:11434');
|
const serverUrl = await db.getSetting('serverUrl', 'http://127.0.0.1:11434');
|
||||||
api = new OllamaAPI(serverUrl);
|
api = new OllamaAPI(serverUrl);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Agent Engine - Agent Loop 核心引擎
|
* Agent Engine - ReAct Agent Loop 核心引擎 (v4.0)
|
||||||
* 管理带工具调用的流式对话循环
|
* ReAct 模式: Thought → Action → Observation → Reflection
|
||||||
|
* 支持任务复杂度评估、自动规划、错误重试、执行轨迹记录
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
@@ -12,35 +13,79 @@ import {
|
|||||||
} from './tool-registry.js';
|
} from './tool-registry.js';
|
||||||
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
||||||
import { showToast } from '../components/toast.js';
|
import { showToast } from '../components/toast.js';
|
||||||
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logStream, logAgentLoop, logModelResponse } from './log-service.js';
|
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logAgentLoop, logModelResponse } from './log-service.js';
|
||||||
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
||||||
|
import { generateId } from '../utils/utils.js';
|
||||||
import type {
|
import type {
|
||||||
OllamaMessage,
|
OllamaMessage,
|
||||||
OllamaStreamChunk,
|
OllamaStreamChunk,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
ToolCallRecord
|
ToolCallRecord,
|
||||||
|
TraceEntry
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|
||||||
const MAX_LOOPS = 10;
|
const MAX_LOOPS = 15;
|
||||||
const MAX_LOOP_TIME = 300000; // 5 分钟总超时
|
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
|
||||||
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
||||||
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
||||||
|
const MAX_RETRIES = 2; // 工具错误自动重试次数
|
||||||
|
|
||||||
|
const REACT_SYSTEM_PROMPT = `你是一个具备 ReAct(Reasoning + Acting)能力的 AI 助手。你必须严格按照以下格式与用户交互:
|
||||||
|
|
||||||
|
## ReAct 输出格式
|
||||||
|
|
||||||
|
每次响应时,你必须按照以下结构思考和行动:
|
||||||
|
|
||||||
|
**Thought:** [你对当前任务的分析和推理。思考用户需要什么、当前有哪些信息、下一步应该做什么。]
|
||||||
|
|
||||||
|
**Action:** [如果需要使用工具,选择一个工具并说明原因。如果不需要工具,直接给出最终答案。]
|
||||||
|
|
||||||
|
**Action Input:** [工具的参数,必须是合法 JSON 格式]
|
||||||
|
|
||||||
|
然后等待 Observation(工具返回的结果)。
|
||||||
|
|
||||||
|
收到 Observation 后:
|
||||||
|
|
||||||
|
**Thought:** [基于工具结果的进一步推理。结果是否满足需求?是否需要更多步骤?]
|
||||||
|
|
||||||
|
**Action:** [下一步行动,或者 "Final Answer" 如果任务已完成]
|
||||||
|
|
||||||
|
**Action Input:** [下一步工具参数,或者最终答案]
|
||||||
|
|
||||||
|
## 任务复杂度评估
|
||||||
|
|
||||||
|
在开始时评估任务:
|
||||||
|
- **简单**:直接回答(1 步),不需要工具或仅需 1 个工具调用
|
||||||
|
- **中等**:需要 2-4 步,可能需要多个工具协作
|
||||||
|
- **复杂**:需要 5+ 步,需要规划和分解
|
||||||
|
|
||||||
|
对于复杂任务,先输出一个简短的执行计划,再开始 ReAct 循环。
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- 如果工具返回错误,分析原因并尝试不同方法
|
||||||
|
- 最多重试 2 次,如果仍然失败,向用户说明情况
|
||||||
|
- 不要反复调用相同参数的相同工具
|
||||||
|
|
||||||
|
## 最终答案
|
||||||
|
|
||||||
|
当任务完成时,输出 **Final Answer:** 后面跟你的最终回答。回答应该:
|
||||||
|
- 汇总所有执行步骤和结果
|
||||||
|
- 直接回答用户的问题
|
||||||
|
- 如果有重要发现,明确指出`;
|
||||||
|
|
||||||
const TOOL_USAGE_GUIDE = `【工具使用规则】
|
const TOOL_USAGE_GUIDE = `【工具使用规则】
|
||||||
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。例如用户之前让你读取了 /home/user/project/config.json,后续说"修改配置"就应继续使用该路径,而不是推测新路径。
|
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。
|
||||||
2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。
|
2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。
|
||||||
3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。
|
3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。
|
||||||
4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。
|
4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。
|
||||||
5. 当用户要求执行命令时,使用 run_command 工具执行。命令通过工作空间终端实时执行并显示,执行完成后将 stdout/stderr 结果告知用户。
|
5. 当用户要求执行命令时,使用 run_command 工具执行。
|
||||||
6. 联网搜索联动:当用户询问需要最新信息的问题时(如版本号、新闻、价格等),先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容获取准确数据。不要仅凭搜索摘要回答——摘要往往不完整,必须抓取原文才能给出准确答案。
|
6. 联网搜索联动:当用户询问需要最新信息的问题时,先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容。
|
||||||
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。以下场景必须使用工具:
|
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。
|
||||||
- 用户问"最新版本"、"现在多少钱"、"最近有什么新闻" → web_search + web_fetch
|
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。
|
||||||
- 用户让你查看/修改/分析文件 → read_file / list_directory / search_files
|
9. 记忆工具:你可以使用 memory_search 搜索过去的记忆,使用 memory_add 添加新的重要记忆。
|
||||||
- 用户问项目状态、代码内容 → git status / read_file
|
10. 会话工具:你可以使用 session_list 列出历史会话,使用 session_read 读取历史会话内容。`;
|
||||||
- 用户让你执行操作 → run_command / write_file 等
|
|
||||||
宁可多调一次工具,也不要给出过时或编造的信息。
|
|
||||||
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。直接使用已有结果继续工作。例如:已经 web_fetch 抓取了页面内容,下一步应直接处理该内容(如 write_file 保存),而不是重新抓取。`;
|
|
||||||
|
|
||||||
/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */
|
/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */
|
||||||
const toolResultCache = new Map<string, ToolResult>();
|
const toolResultCache = new Map<string, ToolResult>();
|
||||||
@@ -48,7 +93,6 @@ const toolResultCache = new Map<string, ToolResult>();
|
|||||||
/** 生成工具调用缓存 key */
|
/** 生成工具调用缓存 key */
|
||||||
function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
||||||
try {
|
try {
|
||||||
// 对 arguments 排序 key 以保证一致性
|
|
||||||
return name + '::' + JSON.stringify(args, Object.keys(args).sort());
|
return name + '::' + JSON.stringify(args, Object.keys(args).sort());
|
||||||
} catch {
|
} catch {
|
||||||
return name + '::' + String(args);
|
return name + '::' + String(args);
|
||||||
@@ -56,10 +100,10 @@ function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 检测当前轮次是否存在重复工具调用 */
|
/** 检测当前轮次是否存在重复工具调用 */
|
||||||
function isDuplicateCall(call: ToolCall, loopCalls: ToolCall[]): boolean {
|
function isDuplicateCall(call: ToolCall, allCalls: ToolCall[]): boolean {
|
||||||
const callKey = getToolCacheKey(call.function.name, call.function.arguments);
|
const callKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||||
return loopCalls.some((prev, idx) => {
|
return allCalls.some((prev, idx) => {
|
||||||
if (idx === loopCalls.length - 1) return false; // 跳过自身
|
if (idx === allCalls.length - 1) return false;
|
||||||
return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey;
|
return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,6 +118,26 @@ export interface AgentCallbacks {
|
|||||||
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存执行轨迹到 SQLite */
|
||||||
|
async function saveTrace(trace: Omit<TraceEntry, 'id' | 'createdAt'>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const bridge = (window as any).metonaDesktop;
|
||||||
|
if (!bridge?.db) return;
|
||||||
|
const entry = {
|
||||||
|
id: `trace_${generateId()}`,
|
||||||
|
session_id: trace.sessionId,
|
||||||
|
step_index: trace.stepIndex,
|
||||||
|
thought: trace.thought,
|
||||||
|
action: trace.action,
|
||||||
|
action_input: trace.actionInput,
|
||||||
|
observation: trace.observation,
|
||||||
|
loop_count: trace.loopCount,
|
||||||
|
created_at: trace.createdAt
|
||||||
|
};
|
||||||
|
await bridge.db.saveTrace(entry);
|
||||||
|
} catch { /* 不阻塞主流程 */ }
|
||||||
|
}
|
||||||
|
|
||||||
export async function runAgentLoop(
|
export async function runAgentLoop(
|
||||||
userContent: string,
|
userContent: string,
|
||||||
images: string[],
|
images: string[],
|
||||||
@@ -82,6 +146,8 @@ export async function runAgentLoop(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const api = state.get<OllamaAPI>(KEYS.API);
|
const api = state.get<OllamaAPI>(KEYS.API);
|
||||||
const model = state.get<string>('_defaultModel', '');
|
const model = state.get<string>('_defaultModel', '');
|
||||||
|
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
||||||
|
const sessionId = currentSession?.id || 'unknown';
|
||||||
|
|
||||||
if (!api || !model) {
|
if (!api || !model) {
|
||||||
showToast('请先选择模型', 'error');
|
showToast('请先选择模型', 'error');
|
||||||
@@ -93,12 +159,8 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
// 检查模型是否支持 Tool Calling
|
// 检查模型是否支持 Tool Calling
|
||||||
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
||||||
if (!modelSupportsTools) {
|
|
||||||
logWarn(`模型 ${model} 不支持 Tool Calling`, 'Agent Loop 可能不稳定');
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages: OllamaMessage[] = [];
|
const messages: OllamaMessage[] = [];
|
||||||
|
|
||||||
let systemPromptParts: string[] = [];
|
let systemPromptParts: string[] = [];
|
||||||
|
|
||||||
// 注入记忆上下文
|
// 注入记忆上下文
|
||||||
@@ -121,12 +183,14 @@ export async function runAgentLoop(
|
|||||||
文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`);
|
文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemPromptParts.length > 0) {
|
// 组合 system prompt
|
||||||
messages.push({ role: 'system', content: systemPromptParts.join('\n\n') + '\n\n' + TOOL_USAGE_GUIDE });
|
const fullSystemPrompt = systemPromptParts.length > 0
|
||||||
} else {
|
? systemPromptParts.join('\n\n') + '\n\n' + REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE
|
||||||
messages.push({ role: 'system', content: TOOL_USAGE_GUIDE });
|
: REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE;
|
||||||
}
|
|
||||||
|
|
||||||
|
messages.push({ role: 'system', content: fullSystemPrompt });
|
||||||
|
|
||||||
|
// 添加历史消息(使用 context-manager 构建)
|
||||||
for (const msg of historyMessages) {
|
for (const msg of historyMessages) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: msg.role as 'user' | 'assistant',
|
role: msg.role as 'user' | 'assistant',
|
||||||
@@ -135,6 +199,7 @@ export async function runAgentLoop(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户消息
|
||||||
const userMsg: OllamaMessage = {
|
const userMsg: OllamaMessage = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: userContent || (images?.length ? (images.length > 1 ? `请分析这 ${images.length} 张图片` : '请分析这张图片') : ''),
|
content: userContent || (images?.length ? (images.length > 1 ? `请分析这 ${images.length} 张图片` : '请分析这张图片') : ''),
|
||||||
@@ -145,18 +210,19 @@ export async function runAgentLoop(
|
|||||||
const tools = getEnabledToolDefinitions();
|
const tools = getEnabledToolDefinitions();
|
||||||
const useTools = tools.length > 0;
|
const useTools = tools.length > 0;
|
||||||
|
|
||||||
logInfo(`Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
|
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
|
||||||
|
|
||||||
let loopCount = 0;
|
let loopCount = 0;
|
||||||
const allToolRecords: ToolCallRecord[] = [];
|
const allToolRecords: ToolCallRecord[] = [];
|
||||||
const loopStartTime = Date.now();
|
const loopStartTime = Date.now();
|
||||||
let content = '';
|
let content = '';
|
||||||
let totalEvalCount = 0;
|
let totalEvalCount = 0;
|
||||||
/** 记录前一轮的工具调用 key,用于跨轮去重 */
|
|
||||||
let prevLoopToolKeys: string[] = [];
|
let prevLoopToolKeys: string[] = [];
|
||||||
|
/** 每轮工具调用计数(用于去重) */
|
||||||
|
let allCallsThisLoop: ToolCall[] = [];
|
||||||
|
|
||||||
const makeStats = () => {
|
const makeStats = () => {
|
||||||
const totalDuration = (Date.now() - loopStartTime) * 1e6; // 转为纳秒
|
const totalDuration = (Date.now() - loopStartTime) * 1e6;
|
||||||
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
|
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,14 +231,14 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
// 全局超时检查
|
// 全局超时检查
|
||||||
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
|
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
|
||||||
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
|
logWarn('ReAct Agent Loop 达到最大运行时间(10分钟),自动停止');
|
||||||
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
|
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已中止
|
// 检查是否已中止
|
||||||
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
||||||
logInfo('Agent Loop 已中止');
|
logInfo('ReAct Agent Loop 已中止');
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -182,6 +248,7 @@ export async function runAgentLoop(
|
|||||||
let thinking = '';
|
let thinking = '';
|
||||||
content = '';
|
content = '';
|
||||||
const toolCalls: ToolCall[] = [];
|
const toolCalls: ToolCall[] = [];
|
||||||
|
allCallsThisLoop = [];
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
||||||
@@ -205,10 +272,6 @@ export async function runAgentLoop(
|
|||||||
if (chunk.message?.thinking) {
|
if (chunk.message?.thinking) {
|
||||||
thinking += chunk.message.thinking;
|
thinking += chunk.message.thinking;
|
||||||
callbacks.onThinking(thinking);
|
callbacks.onThinking(thinking);
|
||||||
// 每收到 200 字符记录一次思考日志
|
|
||||||
if (thinking.length % 200 < 10) {
|
|
||||||
logThink(thinking);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (chunk.message?.content) {
|
if (chunk.message?.content) {
|
||||||
content += chunk.message.content;
|
content += chunk.message.content;
|
||||||
@@ -218,7 +281,6 @@ export async function runAgentLoop(
|
|||||||
if (chunk.message?.tool_calls?.length) {
|
if (chunk.message?.tool_calls?.length) {
|
||||||
for (const tc of chunk.message.tool_calls) {
|
for (const tc of chunk.message.tool_calls) {
|
||||||
if (tc.function?.name) {
|
if (tc.function?.name) {
|
||||||
// 新的工具调用
|
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
@@ -227,7 +289,6 @@ export async function runAgentLoop(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (toolCalls.length > 0) {
|
} else if (toolCalls.length > 0) {
|
||||||
// 增量 arguments 追加到当前最后一个 tool_call
|
|
||||||
const last = toolCalls[toolCalls.length - 1];
|
const last = toolCalls[toolCalls.length - 1];
|
||||||
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
|
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
|
||||||
Object.assign(last.function.arguments, tc.function.arguments);
|
Object.assign(last.function.arguments, tc.function.arguments);
|
||||||
@@ -246,24 +307,15 @@ export async function runAgentLoop(
|
|||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
logInfo('流式调用已中止');
|
logInfo('流式调用已中止');
|
||||||
if (content || thinking) {
|
if (content || thinking) {
|
||||||
messages.push({
|
messages.push({ role: 'assistant', content, ...(thinking && { thinking }) });
|
||||||
role: 'assistant',
|
|
||||||
content,
|
|
||||||
...(thinking && { thinking })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 超时错误 — 保留已生成的内容
|
|
||||||
if ((err as Error).message.includes('超时')) {
|
if ((err as Error).message.includes('超时')) {
|
||||||
logError('流式调用超时(2分钟无数据)', (err as Error).message);
|
logError('流式调用超时', (err as Error).message);
|
||||||
if (content || thinking) {
|
if (content || thinking) {
|
||||||
messages.push({
|
messages.push({ role: 'assistant', content: content || '(模型响应超时)', ...(thinking && { thinking }) });
|
||||||
role: 'assistant',
|
|
||||||
content: content || '(模型响应超时)',
|
|
||||||
...(thinking && { thinking })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||||
return;
|
return;
|
||||||
@@ -271,6 +323,11 @@ export async function runAgentLoop(
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取 ReAct 思考过程
|
||||||
|
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
|
||||||
|
const thought = thoughtMatch ? thoughtMatch[1].trim() : '';
|
||||||
|
|
||||||
|
// 保存 assistant 消息
|
||||||
const assistantMsg: OllamaMessage = {
|
const assistantMsg: OllamaMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content,
|
content,
|
||||||
@@ -278,30 +335,54 @@ export async function runAgentLoop(
|
|||||||
};
|
};
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
assistantMsg.tool_calls = toolCalls;
|
assistantMsg.tool_calls = toolCalls;
|
||||||
// 官方示例要求:tool_calls 时同时保留 content 和 thinking
|
|
||||||
// 确保 content 不为空(模型可能在调用工具前说了话)
|
|
||||||
}
|
}
|
||||||
messages.push(assistantMsg);
|
messages.push(assistantMsg);
|
||||||
|
|
||||||
logModelResponse(content.length, toolCalls.length);
|
logModelResponse(content.length, toolCalls.length);
|
||||||
|
|
||||||
|
// 检查是否是 Final Answer(没有工具调用且包含 Final Answer)
|
||||||
|
const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:');
|
||||||
if (toolCalls.length === 0) {
|
if (toolCalls.length === 0) {
|
||||||
logInfo('无工具调用,Agent Loop 结束');
|
logInfo('无工具调用,ReAct Agent Loop 结束');
|
||||||
|
|
||||||
|
// 自动提取记忆
|
||||||
|
if (isMemoryEnabled() && messages.length >= 4) {
|
||||||
|
try {
|
||||||
|
const { extractMemoriesFromConversation } = await import('./memory-manager.js');
|
||||||
|
await extractMemoriesFromConversation(
|
||||||
|
messages.filter(m => m.role === 'user' || m.role === 'assistant').map(m => ({ role: m.role, content: m.content })),
|
||||||
|
currentSession?.title
|
||||||
|
);
|
||||||
|
} catch { /* 不阻塞 */ }
|
||||||
|
}
|
||||||
|
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跨轮次完整重复检测:如果本轮所有工具调用与上一轮完全相同,终止循环
|
// 跨轮次完整重复检测
|
||||||
const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort();
|
const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort();
|
||||||
const currentKeysStr = JSON.stringify(currentLoopKeys);
|
const currentKeysStr = JSON.stringify(currentLoopKeys);
|
||||||
const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort());
|
const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort());
|
||||||
if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) {
|
if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) {
|
||||||
logWarn('检测到连续两轮工具调用完全相同,终止 Agent Loop', currentKeysStr.slice(0, 200));
|
logWarn('检测到连续两轮工具调用完全相同,终止 ReAct Loop', currentKeysStr.slice(0, 200));
|
||||||
callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats());
|
callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prevLoopToolKeys = currentLoopKeys;
|
prevLoopToolKeys = currentLoopKeys;
|
||||||
|
|
||||||
|
// 记录 ReAct 轨迹
|
||||||
|
const traceStep = {
|
||||||
|
sessionId,
|
||||||
|
stepIndex: loopCount,
|
||||||
|
thought: thought || content.slice(0, 200),
|
||||||
|
action: toolCalls.map(t => t.function.name).join(', '),
|
||||||
|
actionInput: JSON.stringify(toolCalls.map(t => t.function.arguments)),
|
||||||
|
observation: '',
|
||||||
|
loopCount,
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
for (const call of toolCalls) {
|
for (const call of toolCalls) {
|
||||||
// 检查是否已中止
|
// 检查是否已中止
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
@@ -311,30 +392,22 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
// 同一轮内重复调用检测
|
// 同一轮内重复调用检测
|
||||||
if (isDuplicateCall(call, toolCalls)) {
|
if (isDuplicateCall(call, toolCalls)) {
|
||||||
logWarn(`跳过重复工具调用: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
|
logWarn(`跳过重复工具调用: ${call.function.name}`);
|
||||||
const cachedKey = getToolCacheKey(call.function.name, call.function.arguments);
|
const cachedKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||||
const cached = toolResultCache.get(cachedKey);
|
const cached = toolResultCache.get(cachedKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
messages.push({
|
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cached) });
|
||||||
role: 'tool',
|
|
||||||
tool_name: call.function.name,
|
|
||||||
content: JSON.stringify(cached)
|
|
||||||
});
|
|
||||||
callbacks.onToolCallResult(call.function.name, cached, call);
|
callbacks.onToolCallResult(call.function.name, cached, call);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跨轮次缓存命中检测
|
// 跨轮次缓存命中
|
||||||
const cacheKey = getToolCacheKey(call.function.name, call.function.arguments);
|
const cacheKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||||
const cachedResult = toolResultCache.get(cacheKey);
|
const cachedResult = toolResultCache.get(cacheKey);
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
logInfo(`工具缓存命中: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
|
logInfo(`工具缓存命中: ${call.function.name}`);
|
||||||
messages.push({
|
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cachedResult) });
|
||||||
role: 'tool',
|
|
||||||
tool_name: call.function.name,
|
|
||||||
content: JSON.stringify(cachedResult)
|
|
||||||
});
|
|
||||||
callbacks.onToolCallResult(call.function.name, cachedResult, call);
|
callbacks.onToolCallResult(call.function.name, cachedResult, call);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -344,13 +417,12 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
if (needsConfirmation(call.function.name)) {
|
if (needsConfirmation(call.function.name)) {
|
||||||
const confirmed = await callbacks.onConfirmTool(call);
|
const confirmed = await callbacks.onConfirmTool(call);
|
||||||
// 确认后再次检查是否已中止
|
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
logWarn(`工具取消: ${call.function.name}`, '用户取消了操作');
|
logWarn(`工具取消: ${call.function.name}`);
|
||||||
const record: ToolCallRecord = {
|
const record: ToolCallRecord = {
|
||||||
name: call.function.name,
|
name: call.function.name,
|
||||||
arguments: call.function.arguments,
|
arguments: call.function.arguments,
|
||||||
@@ -359,77 +431,76 @@ export async function runAgentLoop(
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
allToolRecords.push(record);
|
allToolRecords.push(record);
|
||||||
|
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: '用户取消了操作' }) });
|
||||||
messages.push({
|
|
||||||
role: 'tool',
|
|
||||||
tool_name: call.function.name,
|
|
||||||
content: JSON.stringify({ success: false, error: '用户取消了操作' })
|
|
||||||
});
|
|
||||||
callbacks.onToolCallError(call.function.name, '用户取消', call);
|
callbacks.onToolCallError(call.function.name, '用户取消', call);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 工具执行 + 自动重试
|
||||||
// 工具执行(run_command 无超时,其他工具 30 秒超时)
|
let lastError = '';
|
||||||
logInfo(`执行工具: ${call.function.name}`, JSON.stringify(call.function.arguments));
|
for (let retry = 0; retry <= MAX_RETRIES; retry++) {
|
||||||
let result: ToolResult;
|
try {
|
||||||
if (call.function.name === 'run_command') {
|
let result: ToolResult;
|
||||||
// run_command 通过 workspace IPC 执行,无超时限制
|
if (call.function.name === 'run_command') {
|
||||||
result = await executeTool(call.function.name, call.function.arguments);
|
result = await executeTool(call.function.name, call.function.arguments);
|
||||||
} else {
|
} else {
|
||||||
// 其他工具保持 30 秒超时保护
|
result = await Promise.race([
|
||||||
result = await Promise.race([
|
executeTool(call.function.name, call.function.arguments).catch(err =>
|
||||||
executeTool(call.function.name, call.function.arguments).catch(err =>
|
({ success: false, error: err?.message || String(err) }) as ToolResult
|
||||||
({ success: false, error: err?.message || String(err) }) as ToolResult
|
),
|
||||||
),
|
new Promise<ToolResult>((_, reject) =>
|
||||||
new Promise<ToolResult>((_, reject) =>
|
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
|
||||||
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
|
)
|
||||||
)
|
]);
|
||||||
]);
|
}
|
||||||
}
|
|
||||||
const record: ToolCallRecord = {
|
|
||||||
name: call.function.name,
|
|
||||||
arguments: call.function.arguments,
|
|
||||||
result,
|
|
||||||
status: result.success ? 'success' : 'error',
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
allToolRecords.push(record);
|
|
||||||
|
|
||||||
messages.push({
|
// 成功
|
||||||
role: 'tool',
|
const record: ToolCallRecord = {
|
||||||
tool_name: call.function.name,
|
name: call.function.name,
|
||||||
content: JSON.stringify(result)
|
arguments: call.function.arguments,
|
||||||
});
|
result,
|
||||||
// 缓存成功结果(非 run_command,因为命令执行可能有副作用)
|
status: result.success ? 'success' : 'error',
|
||||||
if (result.success && call.function.name !== 'run_command') {
|
timestamp: Date.now()
|
||||||
toolResultCache.set(cacheKey, result);
|
};
|
||||||
|
allToolRecords.push(record);
|
||||||
|
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(result) });
|
||||||
|
if (result.success && call.function.name !== 'run_command') {
|
||||||
|
toolResultCache.set(cacheKey, result);
|
||||||
|
}
|
||||||
|
callbacks.onToolCallResult(call.function.name, result, call);
|
||||||
|
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
||||||
|
break; // 成功,退出重试循环
|
||||||
|
} catch (err) {
|
||||||
|
lastError = (err as Error).message;
|
||||||
|
if (retry < MAX_RETRIES) {
|
||||||
|
logWarn(`工具重试 ${retry + 1}/${MAX_RETRIES}: ${call.function.name}`, lastError);
|
||||||
|
await new Promise(r => setTimeout(r, 500)); // 短暂延迟后重试
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 最终失败
|
||||||
|
logError(`工具执行失败: ${call.function.name}`, lastError);
|
||||||
|
const record: ToolCallRecord = {
|
||||||
|
name: call.function.name,
|
||||||
|
arguments: call.function.arguments,
|
||||||
|
result: { success: false, error: lastError },
|
||||||
|
status: 'error',
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
allToolRecords.push(record);
|
||||||
|
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: lastError }) });
|
||||||
|
callbacks.onToolCallError(call.function.name, lastError, call);
|
||||||
}
|
}
|
||||||
callbacks.onToolCallResult(call.function.name, result, call);
|
|
||||||
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = (err as Error).message;
|
|
||||||
logError(`工具执行失败: ${call.function.name}`, errorMsg);
|
|
||||||
const record: ToolCallRecord = {
|
|
||||||
name: call.function.name,
|
|
||||||
arguments: call.function.arguments,
|
|
||||||
result: { success: false, error: errorMsg },
|
|
||||||
status: 'error',
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
allToolRecords.push(record);
|
|
||||||
|
|
||||||
messages.push({
|
|
||||||
role: 'tool',
|
|
||||||
tool_name: call.function.name,
|
|
||||||
content: JSON.stringify({ success: false, error: errorMsg })
|
|
||||||
});
|
|
||||||
callbacks.onToolCallError(call.function.name, errorMsg, call);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存本轮轨迹
|
||||||
|
traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r =>
|
||||||
|
`${r.name}: ${r.result?.success ? 'success' : 'error'}`
|
||||||
|
).join('; ');
|
||||||
|
saveTrace(traceStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
logWarn('Agent Loop 达到最大工具调用次数限制');
|
logWarn('ReAct Agent Loop 达到最大工具调用次数限制');
|
||||||
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
|
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Context Manager - 智能上下文管理 (v4.0)
|
||||||
|
* 三层策略:滑动窗口 + 消息摘要压缩 + 记忆注入
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OllamaMessage } from '../types.js';
|
||||||
|
import { logInfo, logWarn } from './log-service.js';
|
||||||
|
|
||||||
|
/** 粗略估算 token 数 */
|
||||||
|
export function estimateTokens(text: string): number {
|
||||||
|
if (!text) return 0;
|
||||||
|
// 中文按 1.5 字/token,英文按 4 字符/token
|
||||||
|
let chineseChars = 0;
|
||||||
|
let otherChars = 0;
|
||||||
|
for (const ch of text) {
|
||||||
|
if (/[\u4e00-\u9fff]/.test(ch)) chineseChars++;
|
||||||
|
else otherChars++;
|
||||||
|
}
|
||||||
|
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextBuildOptions {
|
||||||
|
/** 滑动窗口大小(最近 N 条消息完整保留) */
|
||||||
|
windowSize?: number;
|
||||||
|
/** 每 N 条更早的消息压缩为一段摘要 */
|
||||||
|
summaryBatchSize?: number;
|
||||||
|
/** 最大 token 数限制 */
|
||||||
|
maxTokens?: number;
|
||||||
|
/** 系统 prompt 注入的记忆上下文 */
|
||||||
|
memoryContext?: string;
|
||||||
|
/** 工作空间上下文 */
|
||||||
|
workspaceContext?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS: Required<ContextBuildOptions> = {
|
||||||
|
windowSize: 20,
|
||||||
|
summaryBatchSize: 20,
|
||||||
|
maxTokens: 24576,
|
||||||
|
memoryContext: '',
|
||||||
|
workspaceContext: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建发送给模型的 messages
|
||||||
|
* 三层策略:
|
||||||
|
* a. 滑动窗口:最近 N 条消息完整保留
|
||||||
|
* b. 更早的消息:每 N 条压缩为一段摘要
|
||||||
|
* c. 系统 prompt 注入记忆上下文
|
||||||
|
*/
|
||||||
|
export function buildContext(
|
||||||
|
allMessages: OllamaMessage[],
|
||||||
|
options: ContextBuildOptions = {}
|
||||||
|
): OllamaMessage[] {
|
||||||
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
const result: OllamaMessage[] = [];
|
||||||
|
|
||||||
|
// 系统 prompt
|
||||||
|
let systemContent = '';
|
||||||
|
if (opts.memoryContext) {
|
||||||
|
systemContent += opts.memoryContext + '\n\n';
|
||||||
|
}
|
||||||
|
if (opts.workspaceContext) {
|
||||||
|
systemContent += opts.workspaceContext + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取已有的 system 消息
|
||||||
|
const existingSystem = allMessages.find(m => m.role === 'system');
|
||||||
|
if (existingSystem) {
|
||||||
|
systemContent += existingSystem.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemContent.trim()) {
|
||||||
|
result.push({ role: 'system', content: systemContent.trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 system 消息
|
||||||
|
const nonSystemMessages = allMessages.filter(m => m.role !== 'system');
|
||||||
|
|
||||||
|
if (nonSystemMessages.length <= opts.windowSize) {
|
||||||
|
// 消息不多,全部保留
|
||||||
|
result.push(...nonSystemMessages);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑动窗口:最近 N 条
|
||||||
|
const recentMessages = nonSystemMessages.slice(-opts.windowSize);
|
||||||
|
|
||||||
|
// 更早的消息:压缩为摘要
|
||||||
|
const olderMessages = nonSystemMessages.slice(0, -opts.windowSize);
|
||||||
|
const summaries = summarizeOlderMessages(olderMessages, opts.summaryBatchSize);
|
||||||
|
|
||||||
|
result.push(...summaries, ...recentMessages);
|
||||||
|
|
||||||
|
// Token 估算和裁剪
|
||||||
|
const trimmed = trimByTokenLimit(result, opts.maxTokens);
|
||||||
|
|
||||||
|
logInfo(`上下文构建: ${nonSystemMessages.length} 条消息 → ${trimmed.length} 条 (窗口: ${opts.windowSize})`,
|
||||||
|
`估算 tokens: ${estimateTokens(trimmed.map(m => m.content).join(''))}`);
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将较早的消息每 batchSize 条压缩为一段摘要
|
||||||
|
*/
|
||||||
|
function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] {
|
||||||
|
const summaries: OllamaMessage[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length; i += batchSize) {
|
||||||
|
const batch = messages.slice(i, i + batchSize);
|
||||||
|
const summary = createQuickSummary(batch);
|
||||||
|
summaries.push({
|
||||||
|
role: 'system',
|
||||||
|
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速摘要(不调用模型,直接提取关键信息)
|
||||||
|
*/
|
||||||
|
function createQuickSummary(messages: OllamaMessage[]): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
const role = msg.role === 'user' ? '用户' : 'AI';
|
||||||
|
const content = msg.content || '';
|
||||||
|
// 取前 100 字符作为摘要
|
||||||
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
||||||
|
if (preview.trim()) {
|
||||||
|
parts.push(`${role}: ${preview}`);
|
||||||
|
}
|
||||||
|
// 如果有工具调用,记录工具名
|
||||||
|
if (msg.tool_calls?.length) {
|
||||||
|
const toolNames = msg.tool_calls.map(t => t.function.name).join(', ');
|
||||||
|
parts.push(` [工具: ${toolNames}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 token 限制裁剪消息
|
||||||
|
*/
|
||||||
|
function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaMessage[] {
|
||||||
|
let totalTokens = 0;
|
||||||
|
const result: OllamaMessage[] = [];
|
||||||
|
|
||||||
|
// 从后往前添加(优先保留最近的消息)
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
const msgTokens = estimateTokens(msg.content || '') +
|
||||||
|
(msg.images ? msg.images.length * 100 : 0) +
|
||||||
|
(msg.tool_calls ? msg.tool_calls.length * 50 : 0);
|
||||||
|
|
||||||
|
// system 消息始终保留
|
||||||
|
if (msg.role === 'system') {
|
||||||
|
totalTokens += msgTokens;
|
||||||
|
result.unshift(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalTokens + msgTokens > maxTokens && result.length > 2) {
|
||||||
|
break; // 已到限制,保留至少 system + 1 条消息
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTokens += msgTokens;
|
||||||
|
result.unshift(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -350,6 +350,71 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// v4.0 新增工具:记忆和会话管理
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'memory_search',
|
||||||
|
description: 'Search agent memories by semantic similarity or keywords. Returns relevant memories about the user, preferences, rules, and past conversations.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['query'],
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search query for memories.' },
|
||||||
|
limit: { type: 'integer', description: 'Max results to return. Default: 8.' },
|
||||||
|
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Filter by memory type.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'memory_add',
|
||||||
|
description: 'Add a new memory entry for future reference. Use when you learn something important about the user, their preferences, or rules they want you to follow.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['type', 'content'],
|
||||||
|
properties: {
|
||||||
|
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Memory type: fact (about user), preference (user preference), rule (must follow).' },
|
||||||
|
content: { type: 'string', description: 'The memory content. Be concise and specific.' },
|
||||||
|
importance: { type: 'integer', description: 'Importance 1-10. Higher = more important. Default: 5.' },
|
||||||
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for search matching. 2-5 keywords.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'session_list',
|
||||||
|
description: 'List previous chat sessions with their titles and timestamps. Useful for referencing past conversations.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', description: 'Max sessions to return. Default: 20.' },
|
||||||
|
search: { type: 'string', description: 'Filter sessions by title keyword.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'session_read',
|
||||||
|
description: 'Read the messages from a previous chat session. Use when the user references something from a past conversation.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['session_id'],
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'The session ID to read.' },
|
||||||
|
max_messages: { type: 'integer', description: 'Max messages to return. Default: 50.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -380,7 +445,8 @@ let enabledTools: Set<string> = new Set([
|
|||||||
'run_command',
|
'run_command',
|
||||||
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
||||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||||
'read_multiple_files', 'git', 'compress'
|
'read_multiple_files', 'git', 'compress',
|
||||||
|
'memory_search', 'memory_add', 'session_list', 'session_read'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
||||||
@@ -409,6 +475,70 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 内存工具(不走 IPC,直接处理)
|
||||||
|
if (toolName === 'memory_search') {
|
||||||
|
const { searchMemories } = await import('./memory-manager.js');
|
||||||
|
const query = args.query as string;
|
||||||
|
const limit = (args.limit as number) || 8;
|
||||||
|
const type = args.type as string | undefined;
|
||||||
|
let results = searchMemories(query, limit);
|
||||||
|
if (type) results = results.filter(r => r.type === type);
|
||||||
|
logToolResult('memory_search', true, `${results.length} 条结果`);
|
||||||
|
return { success: true, results, total: results.length };
|
||||||
|
}
|
||||||
|
if (toolName === 'memory_add') {
|
||||||
|
const { addMemory } = await import('./memory-manager.js');
|
||||||
|
const type = args.type as 'fact' | 'preference' | 'rule';
|
||||||
|
const content = args.content as string;
|
||||||
|
const importance = (args.importance as number) || 5;
|
||||||
|
const tags = (args.tags as string[]) || [];
|
||||||
|
if (!content || content.length < 2) {
|
||||||
|
return { success: false, error: 'content 不能为空且至少 2 个字符' };
|
||||||
|
}
|
||||||
|
const entry = await addMemory({ type, content, importance, tags });
|
||||||
|
logToolResult('memory_add', true, `${type}: ${content.slice(0, 50)}`);
|
||||||
|
return { success: true, id: entry.id, type: entry.type, content: entry.content };
|
||||||
|
}
|
||||||
|
if (toolName === 'session_list') {
|
||||||
|
const bridge = window.metonaDesktop;
|
||||||
|
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
||||||
|
const limit = (args.limit as number) || 20;
|
||||||
|
const search = (args.search as string) || '';
|
||||||
|
const sessions = await bridge.db.getAllSessions();
|
||||||
|
let filtered = sessions.map((s: any) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
model: s.model,
|
||||||
|
messageCount: 0, // 从 SQLite 获取的原始行不含 messages
|
||||||
|
createdAt: s.created_at,
|
||||||
|
updatedAt: s.updated_at
|
||||||
|
}));
|
||||||
|
if (search) {
|
||||||
|
filtered = filtered.filter((s: any) => s.title.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
}
|
||||||
|
filtered.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
|
||||||
|
filtered = filtered.slice(0, limit);
|
||||||
|
logToolResult('session_list', true, `${filtered.length} 个会话`);
|
||||||
|
return { success: true, sessions: filtered, total: filtered.length };
|
||||||
|
}
|
||||||
|
if (toolName === 'session_read') {
|
||||||
|
const bridge = window.metonaDesktop;
|
||||||
|
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
||||||
|
const sessionId = args.session_id as string;
|
||||||
|
const maxMessages = (args.max_messages as number) || 50;
|
||||||
|
if (!sessionId) return { success: false, error: '缺少 session_id 参数' };
|
||||||
|
const session = await bridge.db.getSession(sessionId);
|
||||||
|
if (!session) return { success: false, error: `会话 ${sessionId} 不存在` };
|
||||||
|
const messages = await bridge.db.getMessages(sessionId);
|
||||||
|
const msgs = messages.slice(0, maxMessages).map((m: any) => ({
|
||||||
|
role: m.role,
|
||||||
|
content: m.content || '',
|
||||||
|
timestamp: m.created_at
|
||||||
|
}));
|
||||||
|
logToolResult('session_read', true, `${msgs.length} 条消息`);
|
||||||
|
return { success: true, session: { id: session.id, title: session.title, model: session.model }, messages: msgs, total: messages.length };
|
||||||
|
}
|
||||||
|
|
||||||
// run_command 走 workspace IPC
|
// run_command 走 workspace IPC
|
||||||
if (toolName === 'run_command') {
|
if (toolName === 'run_command') {
|
||||||
const command = args.command as string;
|
const command = args.command as string;
|
||||||
@@ -445,54 +575,27 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
|
|
||||||
export function getToolIcon(name: string): string {
|
export function getToolIcon(name: string): string {
|
||||||
const icons: Record<string, string> = {
|
const icons: Record<string, string> = {
|
||||||
read_file: '📄',
|
read_file: '📄', write_file: '✏️', list_directory: '📁',
|
||||||
write_file: '✏️',
|
search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
|
||||||
list_directory: '📁',
|
move_file: '📦', copy_file: '📋', web_fetch: '🌐', append_file: '➕',
|
||||||
search_files: '🔍',
|
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
||||||
create_directory: '📂',
|
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||||
delete_file: '🗑️',
|
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||||
run_command: '💻',
|
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖'
|
||||||
move_file: '📦',
|
|
||||||
copy_file: '📋',
|
|
||||||
web_fetch: '🌐',
|
|
||||||
append_file: '➕',
|
|
||||||
edit_file: '✂️',
|
|
||||||
get_file_info: 'ℹ️',
|
|
||||||
tree: '🌳',
|
|
||||||
download_file: '⬇️',
|
|
||||||
diff_files: '🔀',
|
|
||||||
replace_in_files: '🔄',
|
|
||||||
read_multiple_files: '📚',
|
|
||||||
git: '🔖',
|
|
||||||
compress: '🗜️',
|
|
||||||
web_search: '🔍'
|
|
||||||
};
|
};
|
||||||
return icons[name] || '🔧';
|
return icons[name] || '🔧';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatToolName(name: string): string {
|
export function formatToolName(name: string): string {
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
read_file: '读取文件',
|
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
|
||||||
write_file: '写入文件',
|
search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件',
|
||||||
list_directory: '列出目录',
|
run_command: '执行命令', move_file: '移动文件', copy_file: '复制文件',
|
||||||
search_files: '搜索文件',
|
web_fetch: '网页抓取', append_file: '追加文件', edit_file: '编辑文件',
|
||||||
create_directory: '创建目录',
|
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
||||||
delete_file: '删除文件',
|
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||||
run_command: '执行命令',
|
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||||
move_file: '移动文件',
|
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话'
|
||||||
copy_file: '复制文件',
|
|
||||||
web_fetch: '网页抓取',
|
|
||||||
append_file: '追加文件',
|
|
||||||
edit_file: '编辑文件',
|
|
||||||
get_file_info: '文件信息',
|
|
||||||
tree: '目录树',
|
|
||||||
download_file: '下载文件',
|
|
||||||
diff_files: '文件对比',
|
|
||||||
replace_in_files: '批量替换',
|
|
||||||
read_multiple_files: '批量读取',
|
|
||||||
git: 'Git 操作',
|
|
||||||
compress: '压缩/解压',
|
|
||||||
web_search: '联网搜索'
|
|
||||||
};
|
};
|
||||||
return names[name] || name;
|
return names[name] || name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2952,3 +2952,127 @@ html, body {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════
|
||||||
|
v4.0 ReAct 执行面板样式
|
||||||
|
══════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ReAct 思考过程卡片 */
|
||||||
|
.react-thought-card {
|
||||||
|
background: rgba(138, 109, 255, 0.06);
|
||||||
|
border: 1px solid rgba(138, 109, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.react-thought-card:hover {
|
||||||
|
border-color: rgba(138, 109, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-thought-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a78bfa;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.react-thought-header:hover {
|
||||||
|
background: rgba(138, 109, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-thought-content {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.react-thought-content pre {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ReAct 步骤时间线 */
|
||||||
|
.react-timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.react-timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: rgba(138, 109, 255, 0.2);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-step {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.react-step::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -18px;
|
||||||
|
top: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #a78bfa;
|
||||||
|
border: 2px solid var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-step-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.react-step-label.thought { color: #a78bfa; }
|
||||||
|
.react-step-label.action { color: #60cdff; }
|
||||||
|
.react-step-label.observation { color: #4ade80; }
|
||||||
|
|
||||||
|
.react-step-content {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具调用卡片增强(v4.0 耗时显示) */
|
||||||
|
.tool-call-duration {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ReAct 执行摘要 */
|
||||||
|
.react-summary {
|
||||||
|
background: rgba(74, 222, 128, 0.06);
|
||||||
|
border: 1px solid rgba(74, 222, 128, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.react-summary-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4ade80;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+86
@@ -221,6 +221,7 @@ export interface MetonaDesktopAPI {
|
|||||||
onAppQuit: (callback: () => void) => void;
|
onAppQuit: (callback: () => void) => void;
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => void;
|
onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => void;
|
||||||
|
db: DBAPI;
|
||||||
workspace: {
|
workspace: {
|
||||||
getDir: () => Promise<{ dir: string; defaultDir: string }>;
|
getDir: () => Promise<{ dir: string; defaultDir: string }>;
|
||||||
setDir: (dir: string) => Promise<{ success: boolean; dir?: string; error?: string }>;
|
setDir: (dir: string) => Promise<{ success: boolean; dir?: string; error?: string }>;
|
||||||
@@ -332,6 +333,91 @@ export interface ToolCallRecord {
|
|||||||
|
|
||||||
export type AgentState = 'idle' | 'sending' | 'accumulating' | 'executing' | 'confirming' | 'done';
|
export type AgentState = 'idle' | 'sending' | 'accumulating' | 'executing' | 'confirming' | 'done';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// ReAct Trace 类型 (v4.0)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export interface TraceEntry {
|
||||||
|
id: string;
|
||||||
|
sessionId: string;
|
||||||
|
stepIndex: number;
|
||||||
|
thought: string;
|
||||||
|
action: string;
|
||||||
|
actionInput: string;
|
||||||
|
observation: string;
|
||||||
|
loopCount: number;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// SQLite DB IPC 接口 (v4.0)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
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 DBAPI {
|
||||||
|
saveSession: (session: SessionRow) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||||
|
getSession: (id: string) => Promise<SessionRow | null>;
|
||||||
|
getAllSessions: () => Promise<SessionRow[]>;
|
||||||
|
deleteSession: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
clearAllSessions: () => Promise<{ success: boolean; error?: string }>;
|
||||||
|
saveMessage: (msg: MessageRow) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||||
|
getMessages: (sessionId: string) => Promise<MessageRow[]>;
|
||||||
|
saveMemory: (entry: MemoryRow) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||||
|
getMemory: (id: string) => Promise<MemoryRow | null>;
|
||||||
|
getAllMemories: () => Promise<MemoryRow[]>;
|
||||||
|
getMemoriesByType: (type: string) => Promise<MemoryRow[]>;
|
||||||
|
deleteMemory: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
clearAllMemories: () => Promise<{ success: boolean; error?: string }>;
|
||||||
|
searchMemories: (query: string, limit?: number) => Promise<MemoryRow[]>;
|
||||||
|
saveSetting: (key: string, value: unknown) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
getSetting: <T = unknown>(key: string, defaultValue?: T) => Promise<T>;
|
||||||
|
saveToolCall: (tc: unknown) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||||
|
getToolCalls: (sessionId: string) => Promise<unknown[]>;
|
||||||
|
saveTrace: (trace: unknown) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||||
|
getTraces: (sessionId: string) => Promise<TraceEntry[]>;
|
||||||
|
exportSessions: () => Promise<unknown>;
|
||||||
|
importSessions: (data: unknown) => Promise<{ imported: number; skipped: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Window global 声明 ──
|
// ── Window global 声明 ──
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
Reference in New Issue
Block a user