fix(webui): disable dashboard auto update checks
feat(plugins): add plugin marketplace route
This commit is contained in:
57
dashboard/package-lock.json
generated
57
dashboard/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "maibot-dashboard",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "maibot-dashboard",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.10",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
@@ -211,6 +211,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -646,6 +647,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
|
||||
"integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -741,6 +743,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -789,6 +792,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -834,6 +838,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -1260,7 +1265,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1282,7 +1286,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1299,7 +1302,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1314,7 +1316,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -4852,6 +4853,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.10.tgz",
|
||||
"integrity": "sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.161.6",
|
||||
"@tanstack/react-store": "^0.9.3",
|
||||
@@ -4937,6 +4939,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.9.tgz",
|
||||
"integrity": "sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.161.6",
|
||||
"cookie-es": "^2.0.0",
|
||||
@@ -5035,6 +5038,7 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -5587,6 +5591,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -5597,6 +5602,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5693,6 +5699,7 @@
|
||||
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
@@ -6011,6 +6018,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-5.2.0.tgz",
|
||||
"integrity": "sha512-uvfNyz4cnaplt7LYJmEZHuqOuav0tKp4a9WKJIaH6iIj7XiqYvS2J5SEByexAlUFlzefOAyjzj4Ja2dd/8aMrw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "^0.3.4",
|
||||
"@uppy/store-default": "^5.0.0",
|
||||
@@ -6027,6 +6035,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-5.1.1.tgz",
|
||||
"integrity": "sha512-6H/xVvhhdfwp1+FRMp2C+tudyaedqD5+LMDB8Iw20k9+QCL1eGzOh4wXm6MCqJtNfQ1tLaprGMG1jlo7yS/uyw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "^0.3.4",
|
||||
"@uppy/provider-views": "^5.2.2",
|
||||
@@ -6278,6 +6287,7 @@
|
||||
"integrity": "sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.2",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -6342,6 +6352,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6375,6 +6386,7 @@
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -7024,6 +7036,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -7805,8 +7818,7 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@@ -7897,7 +7909,8 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
@@ -8002,6 +8015,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -8462,6 +8476,7 @@
|
||||
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.8.1",
|
||||
"builder-util": "26.8.1",
|
||||
@@ -8839,7 +8854,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -8860,7 +8874,6 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -9182,6 +9195,7 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10641,6 +10655,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2"
|
||||
},
|
||||
@@ -10735,6 +10750,7 @@
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -11425,6 +11441,7 @@
|
||||
"integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.31",
|
||||
"@asamuzakjp/dom-selector": "^6.8.1",
|
||||
@@ -13207,7 +13224,6 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -13877,6 +13893,7 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13964,7 +13981,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -13982,7 +13998,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -14013,6 +14028,7 @@
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -14273,6 +14289,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14304,6 +14321,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -14443,6 +14461,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -14634,7 +14653,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -14894,7 +14914,6 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -15154,6 +15173,7 @@
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz",
|
||||
"integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -15895,7 +15915,6 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -16284,6 +16303,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16709,6 +16729,7 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17268,6 +17289,7 @@
|
||||
"integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.2",
|
||||
"@vitest/mocker": "4.1.2",
|
||||
@@ -17719,6 +17741,7 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { getDashboardVersionStatus, type DashboardVersionStatus } from '@/lib/system-api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { APP_VERSION, formatVersion } from '@/lib/version'
|
||||
import { formatVersion } from '@/lib/version'
|
||||
|
||||
interface LogoAreaProps {
|
||||
sidebarOpen: boolean
|
||||
}
|
||||
|
||||
export function LogoArea({ sidebarOpen }: LogoAreaProps) {
|
||||
const [versionStatus, setVersionStatus] = useState<DashboardVersionStatus | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const loadVersionStatus = async () => {
|
||||
try {
|
||||
const status = await getDashboardVersionStatus(APP_VERSION)
|
||||
if (mounted) {
|
||||
setVersionStatus(status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('检查 WebUI 版本更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
void loadVersionStatus()
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const hasUpdate = versionStatus?.has_update === true && Boolean(versionStatus.latest_version)
|
||||
|
||||
return (
|
||||
<div className="flex h-20 items-center border-b px-4">
|
||||
<div
|
||||
@@ -56,35 +28,7 @@ export function LogoArea({ sidebarOpen }: LogoAreaProps) {
|
||||
<span className="shrink-0 whitespace-nowrap text-sm font-semibold text-primary/70">
|
||||
{formatVersion()}
|
||||
</span>
|
||||
{hasUpdate && (
|
||||
<a
|
||||
href={versionStatus?.pypi_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"inline-flex h-5 min-w-0 items-center rounded-md border border-amber-400/50 px-2",
|
||||
"bg-amber-400/10 text-[11px] font-semibold text-amber-700",
|
||||
"transition-colors hover:bg-amber-400/20 dark:text-amber-300"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">有更新 v{versionStatus?.latest_version}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{false && hasUpdate && (
|
||||
<a
|
||||
href={versionStatus?.pypi_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"inline-flex h-5 items-center rounded-md border border-amber-400/50 px-2",
|
||||
"bg-amber-400/10 text-[11px] font-semibold text-amber-700",
|
||||
"transition-colors hover:bg-amber-400/20 dark:text-amber-300"
|
||||
)}
|
||||
>
|
||||
有更新 v{versionStatus?.latest_version}
|
||||
</a>
|
||||
)}
|
||||
<div className="hidden">
|
||||
<span className="font-bold text-xl text-primary-gradient whitespace-nowrap">MaiBot WebUI</span>
|
||||
<span className="text-base font-semibold text-primary/70 whitespace-nowrap">
|
||||
|
||||
@@ -56,7 +56,7 @@ import { RestartOverlay } from '@/components/restart-overlay'
|
||||
import { ExpressionReviewer } from '@/components/expression-reviewer'
|
||||
import { getBotConfig, getModelConfig } from '@/lib/config-api'
|
||||
import { getReviewStats } from '@/lib/expression-api'
|
||||
import { getDashboardVersionStatus, type DashboardVersionStatus } from '@/lib/system-api'
|
||||
import type { DashboardVersionStatus } from '@/lib/system-api'
|
||||
import { APP_VERSION } from '@/lib/version'
|
||||
import { ZoomableChart } from '@/components/ui/zoomable-chart'
|
||||
|
||||
@@ -132,6 +132,9 @@ interface FeatureStatus {
|
||||
visualEnabled: boolean
|
||||
}
|
||||
|
||||
const MAIBOT_RELEASES_URL = 'https://github.com/Mai-with-u/MaiBot/releases'
|
||||
const MAIBOT_WEBUI_RELEASES_URL = 'https://pypi.org/project/maibot-dashboard/'
|
||||
|
||||
// 为饼图生成更丰富的颜色方案 (HSL色相均匀分布)
|
||||
const generatePieColors = (count: number): string[] => {
|
||||
const colors: string[] = []
|
||||
@@ -200,48 +203,20 @@ function IndexPageContent() {
|
||||
let mounted = true
|
||||
|
||||
const loadLatestVersions = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/Mai-with-u/MaiBot/releases?per_page=20', {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub release status ${response.status}`)
|
||||
}
|
||||
const releases = await response.json() as Array<{
|
||||
draft?: boolean
|
||||
prerelease?: boolean
|
||||
tag_name?: string
|
||||
html_url?: string
|
||||
}>
|
||||
const visibleReleases = releases.filter((release) => !release.draft)
|
||||
const stableRelease = visibleReleases.find((release) => !release.prerelease)
|
||||
const testRelease = visibleReleases[0]
|
||||
if (mounted) {
|
||||
if (stableRelease?.tag_name) {
|
||||
setMaibotStableRelease({
|
||||
version: String(stableRelease.tag_name).replace(/^v/i, '').trim(),
|
||||
url: stableRelease.html_url || 'https://github.com/Mai-with-u/MaiBot/releases',
|
||||
})
|
||||
}
|
||||
if (testRelease?.tag_name) {
|
||||
setMaibotTestRelease({
|
||||
version: String(testRelease.tag_name).replace(/^v/i, '').trim(),
|
||||
url: testRelease.html_url || 'https://github.com/Mai-with-u/MaiBot/releases',
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('检查 MaiBot 最新版本失败:', error)
|
||||
if (!mounted) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await getDashboardVersionStatus(APP_VERSION)
|
||||
if (mounted) {
|
||||
setDashboardVersionStatus(status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('妫€鏌?WebUI 鐗堟湰鏇存柊澶辫触:', error)
|
||||
}
|
||||
// 阶段 0:停止首页主动进行远程版本探测,仅保留发布页跳转入口。
|
||||
setMaibotStableRelease(null)
|
||||
setMaibotTestRelease(null)
|
||||
setDashboardVersionStatus({
|
||||
current_version: APP_VERSION,
|
||||
latest_version: null,
|
||||
has_update: false,
|
||||
package_name: 'maibot-dashboard',
|
||||
pypi_url: MAIBOT_WEBUI_RELEASES_URL,
|
||||
})
|
||||
}
|
||||
|
||||
void loadLatestVersions()
|
||||
@@ -631,7 +606,7 @@ function IndexPageContent() {
|
||||
</div>
|
||||
<div className="hidden">
|
||||
<a
|
||||
href={maibotTestRelease?.url || 'https://github.com/Mai-with-u/MaiBot/releases'}
|
||||
href={maibotTestRelease?.url || MAIBOT_RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 transition-colors hover:text-muted-foreground"
|
||||
@@ -642,38 +617,38 @@ function IndexPageContent() {
|
||||
</div>
|
||||
<div className="space-y-1 border-t border-border/50 pt-2 text-xs text-muted-foreground/60">
|
||||
<a
|
||||
href={maibotStableRelease?.url || 'https://github.com/Mai-with-u/MaiBot/releases'}
|
||||
href={maibotStableRelease?.url || MAIBOT_RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between gap-2 transition-colors hover:text-muted-foreground"
|
||||
>
|
||||
<span>正式版最新</span>
|
||||
<span>查看主程序发布页</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{maibotStableRelease ? `v${maibotStableRelease.version}` : 'GitHub Releases'}
|
||||
GitHub Releases
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href={maibotTestRelease?.url || 'https://github.com/Mai-with-u/MaiBot/releases'}
|
||||
href={maibotTestRelease?.url || MAIBOT_RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between gap-2 transition-colors hover:text-muted-foreground"
|
||||
>
|
||||
<span>测试版最新</span>
|
||||
<span>查看测试版发布页</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{maibotTestRelease ? `v${maibotTestRelease.version}` : 'GitHub Releases'}
|
||||
GitHub Releases
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href={dashboardVersionStatus?.pypi_url || 'https://pypi.org/project/maibot-dashboard/'}
|
||||
href={dashboardVersionStatus?.pypi_url || MAIBOT_WEBUI_RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between gap-2 transition-colors hover:text-muted-foreground"
|
||||
>
|
||||
<span>WebUI 最新</span>
|
||||
<span>查看 WebUI 发布页</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
v{dashboardVersionStatus?.latest_version || APP_VERSION}
|
||||
PyPI
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</span>
|
||||
</a>
|
||||
|
||||
12
src/main.py
12
src/main.py
@@ -19,8 +19,6 @@ from src.manager.async_task_manager import async_task_manager
|
||||
from src.plugin_runtime.integration import get_plugin_runtime_manager
|
||||
from src.prompt.prompt_manager import prompt_manager
|
||||
from src.services.memory_flow_service import memory_automation_service
|
||||
from src.webui.dashboard_update import auto_update_dashboard_if_needed
|
||||
|
||||
# from src.api.main import start_api_server
|
||||
|
||||
# 导入插件运行时
|
||||
@@ -88,15 +86,7 @@ class MainSystem:
|
||||
"""启动时自动检查并更新 WebUI dashboard。"""
|
||||
if not global_config.webui.enabled:
|
||||
return
|
||||
if not global_config.webui.auto_update_dashboard:
|
||||
logger.info("WebUI dashboard 自动更新已关闭")
|
||||
return
|
||||
|
||||
result = await auto_update_dashboard_if_needed()
|
||||
if result.updated:
|
||||
logger.info(result.message)
|
||||
elif result.checked:
|
||||
logger.info(result.message)
|
||||
logger.info("当前分支已禁用 WebUI dashboard 自动更新检查")
|
||||
|
||||
async def _init_components(self) -> None:
|
||||
"""初始化其他组件"""
|
||||
|
||||
@@ -20,12 +20,7 @@ from src.common.database.database import engine, get_db_session
|
||||
from src.common.database.database_model import Images, ImageType
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import MMC_VERSION
|
||||
from src.webui.dashboard_update import (
|
||||
DASHBOARD_PACKAGE_NAME,
|
||||
PYPI_PROJECT_URL,
|
||||
detect_package_runner,
|
||||
get_dashboard_version_info,
|
||||
)
|
||||
from src.webui.dashboard_update import DASHBOARD_PACKAGE_NAME, PYPI_PROJECT_URL
|
||||
from src.webui.dependencies import require_auth
|
||||
|
||||
router = APIRouter(prefix="/system", tags=["system"], dependencies=[Depends(require_auth)])
|
||||
@@ -129,31 +124,6 @@ class LocalCacheCleanupResponse(BaseModel):
|
||||
removed_records: int = 0
|
||||
|
||||
|
||||
def _parse_version_parts(version: str | None) -> Optional[list[int]]:
|
||||
"""将版本号转换为可比较的整数列表。"""
|
||||
if not version:
|
||||
return None
|
||||
parts: list[int] = []
|
||||
for raw_part in version.split("."):
|
||||
if not raw_part.isdigit():
|
||||
return None
|
||||
parts.append(int(raw_part))
|
||||
return parts
|
||||
|
||||
|
||||
def _is_newer_version(latest: str | None, current: str | None) -> bool:
|
||||
"""判断 latest 是否新于 current。"""
|
||||
latest_parts = _parse_version_parts(latest)
|
||||
current_parts = _parse_version_parts(current)
|
||||
if latest_parts is None or current_parts is None:
|
||||
return False
|
||||
|
||||
max_len = max(len(latest_parts), len(current_parts))
|
||||
latest_parts.extend([0] * (max_len - len(latest_parts)))
|
||||
current_parts.extend([0] * (max_len - len(current_parts)))
|
||||
return latest_parts > current_parts
|
||||
|
||||
|
||||
def _iter_files(directory: Path) -> list[Path]:
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
return []
|
||||
@@ -327,14 +297,12 @@ async def get_maibot_status():
|
||||
|
||||
@router.get("/dashboard-version", response_model=DashboardVersionResponse)
|
||||
async def get_dashboard_version(current_version: Optional[str] = None):
|
||||
"""获取 WebUI 当前版本和 PyPI 最新版本。"""
|
||||
version_info = await get_dashboard_version_info(current_version)
|
||||
|
||||
"""获取 WebUI 当前版本信息,不执行远程更新探测。"""
|
||||
return DashboardVersionResponse(
|
||||
current_version=version_info.current_version,
|
||||
latest_version=version_info.latest_version,
|
||||
has_update=version_info.has_update,
|
||||
runner=detect_package_runner(),
|
||||
current_version=current_version or "unknown",
|
||||
latest_version=None,
|
||||
has_update=False,
|
||||
runner="disabled",
|
||||
)
|
||||
|
||||
|
||||
|
||||
804
计划.md
804
计划.md
@@ -1,372 +1,530 @@
|
||||
# 私聊定时跟进 V1 计划
|
||||
# 私聊拟人性增强分阶段计划
|
||||
|
||||
## 当前结论
|
||||
- 本方案只支持私聊,不支持群聊。
|
||||
- 旧草案中的“到点直接发送预先写死的 `message_text`”不再作为 V1 主方案。
|
||||
- V1 改为“定时器负责唤醒当前私聊会话的正常思考流程”,由 LLM 基于到点时的当前上下文决定是否主动发言。
|
||||
- 为了支持“主动发言但不强行回复某条旧消息”,需要新增一个与 `reply` 平行的主动私聊发送工具。
|
||||
- 平台出口、消息存储、Maisaka 上下文写回、Napcat 适配等链路应尽量复用现有实现,不新造一条发送通道。
|
||||
## 总体目标
|
||||
- 在尽量保留当前 maibot 主干能力与最近更新的前提下,增强私聊中的连续感、活人感与主动性。
|
||||
- 优先修复“私聊对象信息容易遗忘”“聊天断开后像失忆”“上下文窗口过于保守”这三类核心问题。
|
||||
- 再逐步增加“定时跟进”“主动私聊”“关系阶段推进”等能力。
|
||||
- 落地时坚持“平行新增、契约尽量不变”:
|
||||
- 不破坏现有 `reply` 语义
|
||||
- 新能力优先通过新工具、新配置、新任务表、新 metadata 增量扩展
|
||||
- 尽量不改现有 WebUI 聊天消息结构与事件结构
|
||||
|
||||
## 目标
|
||||
- 让 LLM 可以在当前私聊里创建一条未来触发的“定时跟进任务”。
|
||||
- 到点后不直接发固定文本,而是重新进入该私聊的正常 planner 思考流程。
|
||||
- 到点思考时直接复用现有上下文窗口、原有 planner prompt、原有工具机制。
|
||||
- 如果 planner 判断现在适合主动发言,则调用新的主动私聊发送工具发出消息。
|
||||
- 如果 planner 判断现在不适合打扰,则可以不发言并直接结束本轮。
|
||||
## 借鉴来源
|
||||
|
||||
## 明确不做
|
||||
- 不做群聊版定时跟进。
|
||||
- 不做跨会话发送;当前私聊中创建的任务,只能唤醒当前私聊,也只能向当前私聊主动发言。
|
||||
- 不做“任意指定目标用户/目标 session 的消息发送器”。
|
||||
- 不做复杂多意图分类,不引入冗余的 `intent_key`。
|
||||
- 不伪造一条新的用户入站消息来驱动到点逻辑。
|
||||
- 不把主动 follow-up 强行伪装成 `reply(msg_id=...)`。
|
||||
### 1. `MoFox-Core`
|
||||
- 主要借鉴:
|
||||
- 私聊对象关系信息常态注入
|
||||
- 主动前护栏判断
|
||||
- `WAITING / IDLE` 状态机思路
|
||||
- 发完后等待回复、超时再判断是否继续发言的活人感逻辑
|
||||
- 不直接照搬:
|
||||
- AFC / KFC 全套 runtime
|
||||
- 强耦合插件系统
|
||||
- 其整套数据库结构
|
||||
|
||||
## 核心方案
|
||||
### 2. `Muice-Chatbot`
|
||||
- 主要借鉴:
|
||||
- 轻量主动开场/问候思路
|
||||
- 随机主动话题池思路
|
||||
- 不直接照搬:
|
||||
- 到点直接发送固定文本
|
||||
- 过于依赖固定 prompt 的主动对话实现
|
||||
|
||||
### 1. 定时器从“文本发送器”改为“流程进入器”
|
||||
- 当前用户在私聊中表达未来某个时间点需要 bot 主动跟进。
|
||||
- Planner 调用新的定时跟进工具,写入一条未来任务。
|
||||
- 到点后,后台调度器不直接发消息,而是唤醒对应私聊会话的 Maisaka planner。
|
||||
- planner 结合当前上下文重新判断:
|
||||
- 是否应该主动发言
|
||||
- 现在该说什么
|
||||
- 是否应该放弃本次跟进
|
||||
- 是否应该重新约下一个时间点
|
||||
## 当前问题
|
||||
- 私聊对话者信息经常会被忘记,应该被常态化注入。
|
||||
- 当前上下文管理偏保守,没充分发挥大上下文模型的能力。
|
||||
- 私聊 runtime 重建后不会自动回灌最近历史,聊天断开一阵子后容易出现“失忆”。
|
||||
- 人物信息、关系信息、共同经历主要依赖临时检索,缺少稳定默认上下文。
|
||||
- planner 查到的信息不能稳定传递到 replyer,导致回复阶段再次遗忘。
|
||||
|
||||
### 2. 新增主动私聊发送工具
|
||||
- 保留现有 `reply`:
|
||||
- 语义仍然是“针对某条已有消息进行回复”
|
||||
- 仍然依赖 `msg_id`
|
||||
- 新增 `send_private_message`:
|
||||
- 语义是“在当前私聊里主动发出一条可见消息”
|
||||
- 不依赖 `msg_id`
|
||||
- 不默认引用旧消息
|
||||
- 这样可以避免 QQ/Napcat 侧展示成“永远在回复某条旧消息”,更符合真实私聊中的自然主动聊天。
|
||||
## 当前准备增加的能力
|
||||
- 定时跟进机制:允许 LLM 在当前私聊中创建未来跟进任务,到点后重新唤醒 planner 判断要不要主动开口。
|
||||
- 主动私聊发送工具:新增不依赖 `msg_id` 的 `send_private_message`,作为 `replyer` 的新发送出口,让主动聊天不再伪装成回复旧消息。
|
||||
- 到点前护栏机制:加入静默时段、最小主动间隔、可选每日上限,避免技术上能发但体验上打扰。
|
||||
- 阶段以及主线机制:允许你在私聊中像玩 galgame 一样,和小麦进行逐步推进的互动。
|
||||
- 为后续私聊状态机预留扩展:后面再补“发完等回复、超时再判断”的活人感机制。
|
||||
|
||||
### 3. 发送出口继续复用现有通道
|
||||
- `send_private_message` 不新造平台出口。
|
||||
- 它和 `reply` 一样,最终复用现有:
|
||||
- `send_service`
|
||||
- `Platform IO`
|
||||
- 现有 driver / adapter
|
||||
- Napcat 出口
|
||||
- 这样发送成功后,仍可复用现有:
|
||||
- 消息写库
|
||||
- memory automation
|
||||
## 兼容性原则
|
||||
|
||||
### 前后端契约
|
||||
- 现有 `reply` 工具保持原语义、原参数、原发送形态不变。
|
||||
- 新增 `send_private_message` 与 `schedule_private_followup` 时,采用与现有 builtin tool 平行的注册、执行、记录方式。
|
||||
- 现有聊天消息 `WsMessage / ChatMessage / MessageSegment` 契约尽量不变:
|
||||
- 主动私聊发出的消息仍然是普通 bot 可见消息
|
||||
- 不新增专用 `MessageSegment.type` 作为前提
|
||||
- 现有工具记录结构尽量不变:
|
||||
- 继续复用现有 tool record / tool result 链路
|
||||
- 新增内容尽量放在 `tool_name`、`tool_data`、metadata 中
|
||||
|
||||
### 配置与 WebUI
|
||||
- 配置面优先平行新增独立配置块,默认值完整,保证旧配置不改也能运行。
|
||||
- WebUI V1 不应成为主阻塞项:
|
||||
- 新功能优先复用现有聊天渲染与工具渲染
|
||||
- 后续再考虑做任务管理页、状态展示页等增强 UI
|
||||
|
||||
---
|
||||
|
||||
## 阶段 0:开发期稳定化
|
||||
|
||||
### 本阶段要解决的问题
|
||||
- 当前准备进入连续多阶段改造,如果开发过程中误触自动更新,容易导致本体与 WebUI 版本漂移。
|
||||
- 一旦前后端版本不一致,后续上下文、工具、调度器相关改造会很难排障。
|
||||
- private-main 开发期间需要一个尽量稳定、可复现的运行基线。
|
||||
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` 断开 WebUI 自动更新检测
|
||||
- `☐` 断开本体自动更新检测
|
||||
- `☐` 避免开发期自动拉取或自动提示更新
|
||||
- `☐` 保留显式手动恢复/更新路径
|
||||
|
||||
### 如何做
|
||||
- 找到当前 WebUI 与本体的更新检查入口:
|
||||
- 启动时自动检查
|
||||
- 定时轮询检查
|
||||
- 页面内自动提示
|
||||
- 开发期优先做“禁用自动检查”,而不是修改版本号或伪造远端结果。
|
||||
- 尽量采用可逆方案:
|
||||
- 配置开关
|
||||
- 环境变量
|
||||
- 显式开发模式判断
|
||||
- 如果本体和 WebUI 的更新逻辑是分开的,也应分别断开,避免只关一边。
|
||||
|
||||
### 借鉴来源
|
||||
- 不依赖 `MoFox-Core` 或 `Muice-Chatbot`。
|
||||
- 这是当前 private-main 连续改造前的工程防护阶段。
|
||||
|
||||
### 验收标准
|
||||
- 启动本体时,不再主动发起自动更新检查。
|
||||
- 启动 WebUI 时,不再自动弹更新提示或自动轮询更新状态。
|
||||
- 本体日志和 WebUI 调试输出中,不再出现自动更新任务被触发的痕迹。
|
||||
- 若后续需要更新,仍存在显式、可控、可手动触发的更新路径。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:修复私聊遗忘问题
|
||||
|
||||
### 本阶段要解决的问题
|
||||
- 私聊对象信息经常被忘记。
|
||||
- 聊天断开后再回来,像从空白状态重新开始。
|
||||
- planner 查到的信息不稳定,replyer 容易再次遗忘。
|
||||
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` 私聊对象信息常态注入
|
||||
- `☐` runtime 重建时自动回灌最近历史
|
||||
- `☐` planner 到 replyer 的信息继承增强
|
||||
- `☐` 上下文选择策略从“纯按条数裁切”调整为“更适合大上下文模型”的策略
|
||||
|
||||
### 本阶段目标预算
|
||||
- 以“大上下文真正可用,但不盲目塞满”为原则,先把私聊 planner 的目标输入预算定在 **256K token 级别**。
|
||||
- 不建议把整整 256K 都让历史消息吃满,应预留系统提示、工具定义、注入块、输出空间。
|
||||
- 第一版建议按下面的思路分配:
|
||||
- 总目标预算:`256K`
|
||||
- 预留给系统提示 / 工具定义 / injected reminders / 输出余量:`32K ~ 64K`
|
||||
- 可用于“历史消息 + 人物信息块 + 共同事件块”的主预算:`160K ~ 220K`
|
||||
- replyer 阶段不必与 planner 一样大,可以显著更小:
|
||||
- 优先依赖 planner 已整理出的参考信息
|
||||
- replyer 可先控制在 `32K ~ 64K` 级别
|
||||
|
||||
### 本阶段要先定下的几个原则
|
||||
- **历史回灌** 不是滑动窗口本身,而是滑动窗口的前置步骤。
|
||||
- **token 滑窗** 不应只做“超了就 pop”,而应做“固定高优先级块保底 + 历史候选池按 token 预算裁切”。
|
||||
- **人物信息块、当前约定、最近共同事件** 的优先级应高于普通旧聊天。
|
||||
- **planner 与 replyer 的上下文策略不必完全一样**:
|
||||
- planner 负责大窗理解
|
||||
- replyer 负责在较小上下文内稳定生成
|
||||
- **必须保留降级路径**:
|
||||
- 即使 tokenizer 暂时不可用,也能退回近似估算模式
|
||||
- 但默认目标是本地 token 预算裁切,而不是继续硬按条数
|
||||
|
||||
### 如何做
|
||||
- 为当前私聊补一个轻量信息聚合入口:
|
||||
- 优先从现有 `person_info`
|
||||
- 再从 A_memorix 的人物画像/关系证据中补充
|
||||
- 统一格式化为稳定上下文块,默认注入 planner
|
||||
- 在 runtime 重建时,按 `session_id` 从消息库回灌最近一段真实聊天历史,而不是从空 `_chat_history` 开始。
|
||||
- replyer 生成阶段不再过度依赖临时工具结果,而是显式承接 planner 已整理出的稳定参考信息。
|
||||
- 调整上下文窗口策略:
|
||||
- 不再只用固定条数近似控制
|
||||
- 引入更大的私聊历史回放范围
|
||||
- 优先保留“人物关系/最近共同事件/当前约定”这类高价值上下文
|
||||
|
||||
### 1.1 runtime 历史重建
|
||||
- 触发时机:
|
||||
- 私聊 runtime 首次创建时
|
||||
- 私聊 runtime 被回收后再次恢复时
|
||||
- 定时跟进任务唤醒私聊 runtime 时
|
||||
- 基本流程:
|
||||
1. 根据 `session_id` 查询最近一段真实消息
|
||||
2. 将消息重新构造成 Maisaka 使用的 `LLMContextMessage`
|
||||
3. 写回 `_chat_history`
|
||||
4. 再叠加本轮新收到的消息与 injected reminders
|
||||
- 第一版建议:
|
||||
- 先取最近 `200 ~ 1000` 条私聊真实消息作为“候选历史池”
|
||||
- 不是全部送给模型,而是交给后续 token 滑窗裁切
|
||||
- 注意:
|
||||
- 历史重建应优先复用现有消息入库格式和 `SessionBackedMessage` 构建逻辑
|
||||
- 不要单独造一套“历史专用消息对象”
|
||||
|
||||
### 1.2 本地 tokenizer / token 预算器
|
||||
- 当前项目里已有 token 使用统计,但那是模型返回 usage 后的结果,不足以做请求前选窗。
|
||||
- 因此本阶段需要新增一个“请求前 token 估算器”抽象层。
|
||||
- 目标不是一开始追求 100% 精确,而是先把“按 token 预算裁切”跑通。
|
||||
- 建议方案:
|
||||
- 新增统一的 `context_token_counter` / `prompt_token_estimator`
|
||||
- 输入为即将送给模型的消息列表或其序列化结果
|
||||
- 输出为估算 token 数
|
||||
- tokenizer 选择建议:
|
||||
- 优先选择与当前 OpenAI-compatible 请求栈兼容的本地 tokenizer 方案
|
||||
- 若误差验证可接受,可直接作为私聊上下文预算器
|
||||
- 若误差过大,再考虑换成更贴近目标模型的 tokenizer
|
||||
- 降级方案:
|
||||
- tokenizer 初始化失败时,可暂时退回“字符数 / 中文字数近似预算”
|
||||
- 但只作为 fallback,不作为主路径
|
||||
|
||||
### 1.3 token 滑动窗口
|
||||
- 目标不是“把 256K 塞满”,而是“尽量稳定地用到 256K 级别能力”。
|
||||
- 建议采用“两层预算”:
|
||||
- 第一层:固定保底块
|
||||
- 第二层:历史滑窗块
|
||||
- 固定保底块建议包含:
|
||||
- 系统提示
|
||||
- 当前私聊对象信息块
|
||||
- 当前约定 / 当前跟进原因
|
||||
- 最近共同事件摘要
|
||||
- 必要的工具定义
|
||||
- 历史滑窗块建议逻辑:
|
||||
1. 从最近消息开始向前累加
|
||||
2. 每加入一条,重新累加 token
|
||||
3. 超预算则停止
|
||||
4. 保证最近几轮真实对话优先进入窗口
|
||||
- 不建议简单地“全拼后从头一直 pop 到合法”作为唯一策略。
|
||||
- 更推荐:
|
||||
- 先固定高优先级块
|
||||
- 再对普通历史做从近到远的预算填充
|
||||
|
||||
### 1.4 高优先级上下文保底
|
||||
- 下面这些信息不应与普通旧聊天放在同一优先级:
|
||||
- 当前私聊对象是谁
|
||||
- 你和对方目前是什么关系
|
||||
- 近期共同事件
|
||||
- 最近承诺/约定
|
||||
- 当前定时跟进原因
|
||||
- 这些内容即使在历史极长的情况下,也应尽量保底进入窗口。
|
||||
- 这部分内容应从:
|
||||
- `person_info`
|
||||
- A_memorix 的人物画像 / 关系证据 / episode
|
||||
- 当前会话内最近承诺
|
||||
统一汇总出来
|
||||
|
||||
### 1.5 planner → replyer 信息继承
|
||||
- 目前 replyer 容易再次遗忘,核心原因之一是它并不稳定承接 planner 已经查到和整理过的信息。
|
||||
- 第一版建议:
|
||||
- planner 输出时显式生成“稳定参考信息块”
|
||||
- replyer 直接消费这块,而不是只依赖再看一遍历史
|
||||
- 这样即使 replyer 窗口比 planner 小很多,也能保持人物信息连续性。
|
||||
|
||||
### 1.6 配置建议
|
||||
- 本阶段建议新增但保持默认兼容的配置项,例如:
|
||||
- `chat.private_context_rebuild_enabled`
|
||||
- `chat.private_context_rebuild_recent_limit`
|
||||
- `chat.private_context_token_budget`
|
||||
- `chat.private_context_reserved_tokens`
|
||||
- `chat.private_context_inject_relation_info`
|
||||
- `chat.private_context_recent_events_limit`
|
||||
- `chat.replyer_context_token_budget`
|
||||
- 默认值应尽量保守,避免对现有用户造成突然的延迟或成本上升。
|
||||
|
||||
### 借鉴来源
|
||||
- `MoFox-Core`
|
||||
- 借鉴其“关系信息 + 印象 + 偏好 +聊天流印象”统一拼装进私聊上下文的思路
|
||||
- `Muice-Chatbot`
|
||||
- 基本不借鉴实现,只参考“最近历史 + 旧记忆回注入”的轻量骨架
|
||||
|
||||
### 验收标准
|
||||
- 同一个私聊会话在沉默一段时间后再次收到消息,bot 不应明显忘记对方是谁。
|
||||
- 重建后的 runtime 能带上最近一段真实聊天历史,而不是只看最后一条消息。
|
||||
- 私聊中不需要用户重复提醒,bot 也能较稳定记住:
|
||||
- 对方称呼
|
||||
- 近期正在聊的事
|
||||
- 最近约定/承诺
|
||||
- 在日志或调试信息里,能看到“重建历史条数”“对象信息注入块”的明确痕迹。
|
||||
- 能在日志或调试信息里看到:
|
||||
- 候选历史条数
|
||||
- 最终入窗条数
|
||||
- 估算 token 数
|
||||
- 被预算裁掉的原因
|
||||
- 在高上下文测试下,planner 能稳定工作在 `256K` 级别预算附近,而不是仍然被 60 条历史限制住。
|
||||
- replyer 即使使用更小窗口,也不应明显丢失 planner 已确认的人物信息。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:定时跟进 V1
|
||||
|
||||
### 本阶段要解决的问题
|
||||
- 当前私聊没有“我之后再来找你”的能力。
|
||||
- 即使用户和 bot 约好了未来某个时间点,系统也无法主动在那时重新思考。
|
||||
- 需要主动发言时,只能强行伪装成 `reply(msg_id=...)`,不自然。
|
||||
- 如果直接让 planner 裸写 `message_text` 并发送,会让主动私聊和普通回复走成两套文案体系。
|
||||
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` 新 builtin tool:`schedule_private_followup`
|
||||
- `☐` 新 builtin tool:`send_private_message`
|
||||
- `☐` 私聊定时跟进任务表
|
||||
- `☐` 后台调度器
|
||||
- `☐` runtime 的“带 reminder 唤醒 planner”入口
|
||||
|
||||
### 如何做
|
||||
- `schedule_private_followup`
|
||||
- 只允许在当前私聊里创建未来任务
|
||||
- 记录触发时间、跟进原因、当时承诺话术
|
||||
- 后台调度器
|
||||
- 轮询 `pending` 任务
|
||||
- 到点后 claim 任务并唤醒当前私聊 runtime
|
||||
- runtime 新入口
|
||||
- 以 `<system-reminder>` 形式注入“这不是用户新消息,而是一次到点跟进触发”
|
||||
- planner 基于当前上下文重新判断要不要发
|
||||
- `send_private_message`
|
||||
- 不直接裸发 `message_text`
|
||||
- 而是作为 `replyer` 的新发送语义出口
|
||||
- 复用现有人设、表达习惯、replyer 文案生成链路
|
||||
- 最终仍复用现有 `send_service` / Platform IO / 写库 / 历史同步
|
||||
|
||||
### 借鉴来源
|
||||
- `MoFox-Core`
|
||||
- 借鉴“主动思考不是直接发消息,而是先重进上下文再判断”的理念
|
||||
- `Muice-Chatbot`
|
||||
- 明确不采用其“到点直接发固定文本”的路径
|
||||
- 当前 maibot 架构
|
||||
- 延续“planner 负责想、replyer 负责写”的职责分离,不让主动私聊成为例外
|
||||
|
||||
### 验收标准
|
||||
- 私聊中可以成功创建一条未来跟进任务。
|
||||
- 到点后系统会唤醒 planner,而不是直接发写死文本。
|
||||
- planner 可以根据当前情况选择:
|
||||
- 主动发一条消息
|
||||
- 什么都不发,直接结束
|
||||
- 再次约下一次跟进
|
||||
- 主动发出的消息是普通 bot 消息,不带旧消息引用。
|
||||
- 主动发出的文本风格应与普通 `reply()` 路径保持一致,不应明显像另一套文案系统。
|
||||
- 发送成功后仍会进入:
|
||||
- 现有消息存储
|
||||
- Maisaka 历史同步
|
||||
- memory automation
|
||||
|
||||
## 端到端流转链路
|
||||
|
||||
### 1. 用户触发
|
||||
- 用户在私聊中表达未来某个时间点需要 bot 主动跟进。
|
||||
- 当前消息照常进入现有链路:
|
||||
- 消息接收
|
||||
- HeartFlow runtime
|
||||
- Timing Gate
|
||||
- Planner
|
||||
|
||||
### 2. Planner 决策
|
||||
- Planner 分析当前局势后,可直接调用定时跟进工具。
|
||||
- 工具应为 planner 直接可见的 action tool,不走 deferred tools。
|
||||
- 同一轮里,planner 仍可继续:
|
||||
- 调用 `reply`
|
||||
- 正常回复用户
|
||||
- 告知用户已经记下这次未来跟进
|
||||
|
||||
### 3. 工具执行
|
||||
- 暂定工具名:`schedule_private_followup`
|
||||
- 工具接收参数后,直接写入任务存储。
|
||||
- 工具只允许操作当前私聊会话,不允许 LLM 指定别的 session。
|
||||
- tool result 与其他工具一样,复用现有 tool call 记录与上下文链路。
|
||||
|
||||
### 4. 后台调度器轮询
|
||||
- 独立后台调度器定期扫描到期任务。
|
||||
- 扫描条件:
|
||||
- `status = pending`
|
||||
- `send_at_ts <= now`
|
||||
- 任务取出后,不直接发送,而是进入“会话唤醒”流程。
|
||||
|
||||
### 5. 会话唤醒
|
||||
- 调度器根据 `session_id` 定位或恢复对应私聊会话。
|
||||
- 如果对应 runtime 当前不在内存中,应先恢复/创建该会话的 Maisaka runtime。
|
||||
- 调度器调用一个新的运行时入口,例如:
|
||||
- `trigger_scheduled_followup(task)`
|
||||
- 或同等语义的方法
|
||||
- 这个入口负责以“额外注入消息”的形式进入 planner,而不是伪造用户消息。
|
||||
|
||||
### 6. 到点后的 planner 行为
|
||||
- 唤醒后的 planner 继续使用项目原有的主 prompt。
|
||||
- 同时在请求尾部追加一条 `injected_user_messages`,内容使用项目已有的 `<system-reminder>` 风格。
|
||||
- planner 基于当前上下文重新分析后,可以:
|
||||
- 调用 `send_private_message`
|
||||
- 调用 `reply`(仅当它明确需要围绕某条旧消息回复时)
|
||||
- 调用 `finish`
|
||||
- 再次调用 `schedule_private_followup`
|
||||
|
||||
### 7. 主动发送
|
||||
- 当 planner 判断应主动发言时,调用 `send_private_message`。
|
||||
- `send_private_message` 直接复用现有 `send_service.text_to_stream_with_message(...)`。
|
||||
- 发送参数应满足:
|
||||
- `stream_id = 当前 ToolExecutionContext.session_id`
|
||||
- `storage_message = True`
|
||||
- `sync_to_maisaka_history = True`
|
||||
- `maisaka_source_kind` 建议使用新的主动发送来源标记,例如:
|
||||
- `proactive_send`
|
||||
- 若后续需要区分定时唤醒来源,可再在 metadata 中补充 `trigger_source=scheduled_followup`
|
||||
|
||||
### 8. 任务完成
|
||||
- 本次唤醒流程成功结束后,原任务应被标记为完成。
|
||||
- “完成”不等于“一定发了消息”。
|
||||
- 只要本次唤醒和 planner 处理已成功结束,即可完成任务,并记录最终动作结果。
|
||||
|
||||
## 工具设计
|
||||
|
||||
### 工具 1:`schedule_private_followup`
|
||||
### `send_private_message` 设计细化
|
||||
|
||||
#### 工具职责
|
||||
- 为当前私聊创建一条未来触发的定时跟进任务。
|
||||
- 任务到点后唤醒当前私聊的 planner,而不是直接发送固定文本。
|
||||
|
||||
#### 工具参数
|
||||
- `send_at`
|
||||
- 类型:`string`
|
||||
- 含义:触发时间
|
||||
- 建议:由 LLM 提供标准化后的绝对时间字符串
|
||||
- `followup_reason`
|
||||
- 类型:`string`
|
||||
- 含义:这次未来跟进的原因/事项摘要
|
||||
- 作用:到点时注入 reminder,帮助 planner 理解为什么会被唤醒
|
||||
- `assistant_commitment_text`
|
||||
- 类型:`string`
|
||||
- 含义:当前这轮里,bot 对用户作出的那句“未来会来找你/提醒你/跟进你”的承诺话术
|
||||
- 作用:到点时也注入给 planner,帮助它和之前的承诺保持一致
|
||||
- `replace_existing`
|
||||
- 类型:`boolean`
|
||||
- 含义:是否覆盖当前私聊中尚未执行的旧跟进任务
|
||||
- 默认建议:`false`
|
||||
|
||||
#### 工具隐含上下文
|
||||
- 不要求 LLM 传 `session_id`
|
||||
- 不要求 LLM 传 `user_id`
|
||||
- 后端直接从当前 `ToolExecutionContext.session_id` 取目标私聊
|
||||
- 如果当前不是私聊,工具直接失败
|
||||
|
||||
#### 工具返回
|
||||
- 成功时至少返回:
|
||||
- `task_id`
|
||||
- `session_id`
|
||||
- `send_at`
|
||||
- `followup_reason`
|
||||
- `assistant_commitment_text`
|
||||
- `replace_existing`
|
||||
- 若发生覆盖,返回被取消的旧任务 ID 列表
|
||||
- 失败时返回明确原因:
|
||||
- 非私聊
|
||||
- 时间非法
|
||||
- 跟进原因为空
|
||||
- 承诺话术为空
|
||||
- 存储失败
|
||||
|
||||
### 工具 2:`send_private_message`
|
||||
|
||||
#### 工具职责
|
||||
- 在当前私聊会话中主动发送一条可见消息。
|
||||
- 在当前私聊会话中主动发出一条可见消息。
|
||||
- 不依赖 `msg_id`。
|
||||
- 不默认带引用回复。
|
||||
- 语义上是“主动开口”,但文案生成仍应走 `replyer`,而不是绕开 replyer 直接裸发。
|
||||
|
||||
#### 工具参数
|
||||
- `message_text`
|
||||
- 类型:`string`
|
||||
- 含义:要主动发送给当前私聊对象的文本内容
|
||||
#### 参数设计建议
|
||||
- 不建议把主参数设计成裸 `message_text`。
|
||||
- 更推荐让 planner 提供“为什么说”,让 replyer 负责“具体怎么说”。
|
||||
- 第一版建议参数:
|
||||
- `proactive_reason`
|
||||
- 类型:`string`
|
||||
- 含义:这次主动发言的直接理由,作为 replyer 的“最新推理”
|
||||
- `reference_info`
|
||||
- 类型:`string`
|
||||
- 含义:本轮主动发言依赖的事实性参考信息
|
||||
- `trigger_source`
|
||||
- 类型:`string`
|
||||
- 含义:触发来源,例如 `scheduled_followup`
|
||||
- `assistant_commitment_text`
|
||||
- 类型:`string`
|
||||
- 含义:若这次主动发言是在延续先前承诺,可作为风格一致性参考
|
||||
|
||||
#### 工具隐含上下文
|
||||
- 不要求 LLM 传 `session_id`
|
||||
- 不允许 LLM 指定别的目标会话
|
||||
- 后端直接使用当前 `ToolExecutionContext.session_id`
|
||||
#### 实现建议
|
||||
- `send_private_message` 应复用当前 `reply()` 已有的 replyer 生成链路。
|
||||
- 推荐执行路径:
|
||||
1. planner 调用 `send_private_message`
|
||||
2. tool 内部获取当前会话 replyer
|
||||
3. 以“主动发言场景”调用 `generate_reply_with_context(...)`
|
||||
4. 此时 `reply_message=None`
|
||||
5. `reply_reason=proactive_reason`
|
||||
6. `reference_info` 中整理注入 `reference_info + assistant_commitment_text + trigger_source`
|
||||
7. replyer 生成文本后,再复用现有发送链路发出
|
||||
|
||||
#### 工具返回
|
||||
#### 返回建议
|
||||
- 成功时至少返回:
|
||||
- `session_id`
|
||||
- `message_text`
|
||||
- `generated_message_text`
|
||||
- `sent_message_id`
|
||||
- `maisaka_source_kind`
|
||||
- 失败时返回明确原因:
|
||||
- 非私聊
|
||||
- 文本为空
|
||||
- replyer 生成失败
|
||||
- 发送失败
|
||||
|
||||
### 工具关系
|
||||
- `reply`:回应某条已有消息
|
||||
- `send_private_message`:主动发言,不依赖某条已有消息
|
||||
- `schedule_private_followup`:创建未来跟进任务,负责到点后唤醒 planner
|
||||
---
|
||||
|
||||
## 任务数据模型
|
||||
## 阶段 3:主动私聊护栏与轻量主动能力
|
||||
|
||||
### 建议字段
|
||||
- `id`
|
||||
- `session_id`
|
||||
- `send_at_ts`
|
||||
- `status`
|
||||
- `followup_reason`
|
||||
- `assistant_commitment_text`
|
||||
- `created_at_ts`
|
||||
- `updated_at_ts`
|
||||
- `created_by_tool_call_id`
|
||||
- `cancelled_by_tool_call_id`
|
||||
- `triggered_at_ts`
|
||||
- `completed_at_ts`
|
||||
- `completion_action`
|
||||
- `sent_message_id`
|
||||
- `last_error`
|
||||
- `replace_existing`
|
||||
### 本阶段要解决的问题
|
||||
- 即使有定时跟进,系统也缺少“主动前的工程护栏”。
|
||||
- 后续如果要做更常态化的主动私聊,没有最小打扰控制会很容易打扰用户。
|
||||
|
||||
### 状态
|
||||
- `pending`
|
||||
- `running`
|
||||
- `completed`
|
||||
- `cancelled`
|
||||
- `failed`
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` 静默时段
|
||||
- `☐` 最小主动间隔
|
||||
- `☐` 可选每日上限
|
||||
- `☐` skip reason 与 guardrail snapshot
|
||||
- `☐` 轻量主动开场/问候 fallback
|
||||
|
||||
### 状态语义
|
||||
- `pending`:已创建,等待到点唤醒
|
||||
- `running`:调度器已取出,正在执行本次唤醒
|
||||
- `completed`:本次唤醒流程已成功结束,无论是否真的发出消息
|
||||
- `cancelled`:被显式取消或被新任务覆盖
|
||||
- `failed`:本次唤醒流程执行失败
|
||||
### 如何做
|
||||
- 在“任务到期”与“真正唤醒 planner”之间插入规则层:
|
||||
- 私聊校验
|
||||
- 状态校验
|
||||
- 静默时段判断
|
||||
- 最小主动间隔判断
|
||||
- 可选每日上限判断
|
||||
- 对被跳过的任务记录:
|
||||
- `skip_reason`
|
||||
- `guardrail_snapshot_json`
|
||||
- 为未来“非约定型主动私聊”预留轻量主动开场能力:
|
||||
- 轻问候
|
||||
- 轻话题冒泡
|
||||
- 但此时仍不做完整主动聊天系统
|
||||
|
||||
### 完成动作建议
|
||||
- `sent_private_message`
|
||||
- `skipped`
|
||||
- `rescheduled`
|
||||
- `unknown`
|
||||
### 借鉴来源
|
||||
- `MoFox-Core`
|
||||
- 重点借鉴其主动思考调度器中的工程护栏思路
|
||||
- `Muice-Chatbot`
|
||||
- 借鉴其轻量问候/轻话题池思路,仅作为 fallback
|
||||
|
||||
## 覆盖规则
|
||||
### 验收标准
|
||||
- 明显不适合打扰的时段,任务会被安全跳过或延后,而不是强行唤醒 planner。
|
||||
- 日志中能明确看到“为什么没发”的 skip reason。
|
||||
- 即使以后加入更强主动私聊,这一层规则也能直接复用。
|
||||
|
||||
### V1 定义
|
||||
- `replace_existing = true` 时:
|
||||
- 将当前 `session_id` 下所有 `pending` 任务置为 `cancelled`
|
||||
- 再创建当前新任务
|
||||
- `replace_existing = false` 时:
|
||||
- 不取消旧任务
|
||||
- 直接新增当前任务
|
||||
---
|
||||
|
||||
### 这样做的原因
|
||||
- 不引入模糊分类
|
||||
- 不猜“哪个旧任务和哪个新任务算同类”
|
||||
- 规则简单、确定、易解释
|
||||
## 阶段 4:私聊状态机活人感增强
|
||||
|
||||
## 到点注入给 planner 的提示方式
|
||||
### 本阶段要解决的问题
|
||||
- 当前主动私聊即使能发,也缺少“发完之后等你回复”的真实感。
|
||||
- 系统无法区分“及时回”“晚回”“一直没回”。
|
||||
|
||||
### 复用原则
|
||||
- 继续复用原有 [prompts/zh-CN/maisaka_chat.prompt](/D:/mai-bot-align-pre17/prompts/zh-CN/maisaka_chat.prompt)
|
||||
- 不复制一整份新 prompt
|
||||
- 不把 reminder 伪装成真实聊天历史消息
|
||||
- 复用现有 `injected_user_messages` 机制
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` `WAITING / IDLE` 私聊状态机
|
||||
- `☐` 主动发送后的等待态
|
||||
- `☐` 超时后再判断是否继续等、追问或闭嘴
|
||||
- `☐` 连续超时计数与打扰保护
|
||||
|
||||
### 风格要求
|
||||
- 注入内容应沿用项目现有 `<system-reminder>...</system-reminder>` 风格
|
||||
- 它在请求里以 `user` 身份追加,但语义上明确说明“不是用户刚刚发来的新消息”
|
||||
- 应提醒 planner:
|
||||
- 这是一次定时跟进触发
|
||||
- 当前为什么会被唤醒
|
||||
- 你之前对用户说过什么
|
||||
- 可以主动发言,也可以不发言
|
||||
- 除非明确需要回应某条旧消息,否则不要默认调用 `reply`
|
||||
### 如何做
|
||||
- 在阶段 2 的基础上,扩展主动发送后的状态记录:
|
||||
- 最近一次主动发送时间
|
||||
- 是否正在等待回复
|
||||
- 等待配置
|
||||
- 连续超时计数
|
||||
- 收到用户消息时区分:
|
||||
- `reply_in_time`
|
||||
- `reply_late`
|
||||
- `new_message`
|
||||
- 等待超时后,再重新进行一轮轻量判断:
|
||||
- 继续等
|
||||
- 轻追问
|
||||
- 结束等待,不再打扰
|
||||
|
||||
### 提示文案打样
|
||||
```text
|
||||
<system-reminder>
|
||||
以下内容不是用户刚刚发来的新消息,而是当前私聊的一次定时跟进触发。
|
||||
### 借鉴来源
|
||||
- `MoFox-Core`
|
||||
- 这是最核心的借鉴来源,重点是其 `WAITING / IDLE` 与超时后再思考的设计
|
||||
- `Muice-Chatbot`
|
||||
- 基本不借鉴
|
||||
|
||||
触发时间:{trigger_at}
|
||||
### 验收标准
|
||||
- 主动发言后,系统能进入等待态,而不是把这件事完全忘掉。
|
||||
- 用户及时回复和很久后才回复时,系统的反应能出现可区分差异。
|
||||
- 多次未回复后,系统会自动降低继续打扰的倾向。
|
||||
|
||||
你之前在这个私聊中设置过一次定时跟进。
|
||||
当时记录的跟进原因:
|
||||
{followup_reason}
|
||||
---
|
||||
|
||||
当时你已经对用户说过:
|
||||
{assistant_commitment_text}
|
||||
## 阶段 5:关系阶段与主线推进
|
||||
|
||||
请结合当前上下文重新分析现在是否适合主动发言:
|
||||
- 如果仍然适合,就继续正常分析,并在需要时调用 send_private_message。
|
||||
- 如果当前话题已经变化、用户已经提到过同类内容、约定已经失效,或者现在不适合打扰,就不要强行发送,直接 finish。
|
||||
- 不要把这段提醒当成用户的新发言。
|
||||
- 除非你明确是在回应历史中的某条具体消息,否则不要默认调用 reply。
|
||||
</system-reminder>
|
||||
```
|
||||
### 本阶段要解决的问题
|
||||
- 当前即使记住了人、能主动聊,也还没有真正的关系推进结构。
|
||||
- 缺少“朋友 → 更亲密阶段”的可持续演进逻辑。
|
||||
|
||||
## 与现有链路的复用点
|
||||
### 本阶段要增加/修改的点
|
||||
- `☐` 关系阶段状态
|
||||
- `☐` 阶段推进条件
|
||||
- `☐` 近期主线事件/共同事件摘要
|
||||
- `☐` 更适合私聊恋爱化推进的上下文注入
|
||||
|
||||
### Prompt 与上下文
|
||||
- 复用 Maisaka planner 主 prompt
|
||||
- 复用现有上下文窗口选择逻辑
|
||||
- 复用 `injected_user_messages` 追加尾部提醒
|
||||
### 如何做
|
||||
- 在现有 `person_info` / A_memorix 证据层之上,增加一层更明确的关系状态表达。
|
||||
- 阶段推进仍然尽量保持轻规则,不急着做全硬编码 romance engine。
|
||||
- 优先做:
|
||||
- 阶段状态
|
||||
- 最近共同事件摘要
|
||||
- 对阶段敏感的 prompt 注入
|
||||
- 后续再看是否要加入更强规则。
|
||||
|
||||
### 发送
|
||||
- `send_private_message` 复用 `send_service.text_to_stream_with_message(...)`
|
||||
- 继续复用现有 `Platform IO` 和 Napcat 出口
|
||||
### 借鉴来源
|
||||
- `MoFox-Core`
|
||||
- 借鉴其“关系分数 + 关系阶段 + 印象文案”的分层思路
|
||||
- 不直接采用其当前阶段命名作为最终产品逻辑
|
||||
- `Muice-Chatbot`
|
||||
- 不足以支撑这一阶段
|
||||
|
||||
### 存储
|
||||
- 复用现有消息发送成功后的消息存储链路
|
||||
### 验收标准
|
||||
- 私聊中的称呼、关心方式、主动发言方式,会随关系阶段出现可感知变化。
|
||||
- 最近共同事件能够被作为关系推进依据,而不是每次都从零开始。
|
||||
- 系统在长期私聊中呈现出更稳定的关系连续性,而不是只有短期记忆连续性。
|
||||
|
||||
### 上下文写回
|
||||
- 复用 `sync_to_maisaka_history=True`
|
||||
- 复用 runtime 的 `append_sent_message_to_chat_history(...)`
|
||||
---
|
||||
|
||||
### Tool call 记录
|
||||
- 创建任务时,复用现有 Planner tool 执行记录链路
|
||||
- 到点唤醒不伪造一条新的 LLM tool call
|
||||
- 到点执行本质上属于调度器唤醒会话,而不是伪造用户发言
|
||||
## 配置与 WebUI 影响评估
|
||||
|
||||
## 需要补的实现模块
|
||||
### 配置面
|
||||
- 配置面预计是“小改”,不是大改。
|
||||
- 建议新增独立配置块,例如:
|
||||
- `private_followup.enabled`
|
||||
- `private_followup.scheduler_interval_seconds`
|
||||
- `private_followup.quiet_hours_start`
|
||||
- `private_followup.quiet_hours_end`
|
||||
- `private_followup.min_interval_between_proactive_seconds`
|
||||
- `private_followup.max_daily_followups_per_session`
|
||||
- `private_followup.inject_relation_info`
|
||||
- `private_followup.recent_shared_events_limit`
|
||||
- 原则:
|
||||
- 默认值完整
|
||||
- 老配置不动也能跑
|
||||
- 优先改模板并递增版本号
|
||||
|
||||
### 1. 新内置工具
|
||||
- 新增 `schedule_private_followup` builtin tool
|
||||
- 新增 `send_private_message` builtin tool
|
||||
### WebUI 面
|
||||
- V1 到 V3 原则上不需要大改聊天协议。
|
||||
- 聊天渲染层:
|
||||
- `send_private_message` 发送的仍然是普通 bot 消息
|
||||
- 不要求新增 `MessageSegment.type`
|
||||
- 工具渲染层:
|
||||
- 新工具复用现有 tool record 展示链路
|
||||
- 前端最多看到新的 `tool_name`
|
||||
- 可选增强:
|
||||
- 定时跟进任务列表
|
||||
- 任务取消/重试管理页
|
||||
- 主动护栏状态可视化
|
||||
- 私聊状态机状态展示
|
||||
|
||||
### 2. 任务存储
|
||||
- 新增一张私聊定时跟进任务表
|
||||
- 提供最小操作:
|
||||
- create
|
||||
- cancel pending by session
|
||||
- list due pending
|
||||
- mark running
|
||||
- mark completed
|
||||
- mark failed
|
||||
## 分阶段验收顺序建议
|
||||
- 第 0 阶段先稳定开发环境,避免自动更新打断改造过程。
|
||||
- 第一阶段先解决遗忘与历史重建,不急着上主动聊天。
|
||||
- 第二阶段打通定时跟进 V1。
|
||||
- 第三阶段补主动护栏与轻量主动能力。
|
||||
- 第四阶段再做 `WAITING / IDLE` 状态机。
|
||||
- 第五阶段最后做关系阶段与主线推进。
|
||||
|
||||
### 3. 后台调度器
|
||||
- 独立异步循环
|
||||
- 固定间隔扫描 due tasks
|
||||
- 负责 claim 任务、唤醒会话、更新状态
|
||||
|
||||
### 4. Maisaka 运行时入口
|
||||
- 为运行时新增一个“带 reminder 进入 planner 一轮”的入口
|
||||
- 该入口负责把 `<system-reminder>` 注入到 planner 请求尾部
|
||||
|
||||
### 5. 主程序启动
|
||||
- 在现有后台任务体系中注册该调度器
|
||||
|
||||
## 最小验证路径
|
||||
|
||||
### 验证目标
|
||||
- 确认创建任务、到点唤醒、重新思考、主动发送、写库、上下文同步都能走通
|
||||
|
||||
### 建议路径
|
||||
1. 在私聊中让 bot 创建一条几分钟后的 `schedule_private_followup`
|
||||
2. 当轮正常 `reply` 给用户,说明之后会再来跟进
|
||||
3. 到点后由调度器唤醒当前私聊的 planner
|
||||
4. planner 收到 `<system-reminder>` 后重新分析
|
||||
5. 若判断合适,调用 `send_private_message`
|
||||
6. 发送应复用现有 Napcat / Platform IO 出口成功发出
|
||||
7. 发出的消息应进入:
|
||||
- 现有消息存储
|
||||
- Maisaka 上下文历史
|
||||
- 现有 memory automation
|
||||
|
||||
## 当前建议的落地顺序
|
||||
- 第一步:先把工具边界定下来:`schedule_private_followup` 与 `send_private_message`
|
||||
- 第二步:把任务表与任务存储定下来
|
||||
- 第三步:补运行时“带 reminder 唤醒 planner”入口
|
||||
- 第四步:补后台调度器并接入主程序
|
||||
- 第五步:打通主动发送、写库、上下文同步
|
||||
- 第六步:再考虑取消/查看任务工具
|
||||
## 当前最推荐的开发顺序
|
||||
1. 断开 WebUI / 本体自动更新检测
|
||||
2. 私聊对象信息常态注入
|
||||
3. runtime 历史重建
|
||||
4. planner → replyer 信息继承增强
|
||||
5. `schedule_private_followup`
|
||||
6. `send_private_message`
|
||||
7. `send_private_message` 的 replyer 驱动实现
|
||||
8. 后台调度器与 runtime 唤醒入口
|
||||
9. 到点前护栏
|
||||
10. `WAITING / IDLE` 扩展
|
||||
11. 阶段/主线推进
|
||||
|
||||
Reference in New Issue
Block a user