diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 77877140..4131f656 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -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" } diff --git a/dashboard/src/components/layout/LogoArea.tsx b/dashboard/src/components/layout/LogoArea.tsx index 9b298b7f..6648dcb4 100644 --- a/dashboard/src/components/layout/LogoArea.tsx +++ b/dashboard/src/components/layout/LogoArea.tsx @@ -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(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 (
{formatVersion()} - {hasUpdate && ( - - 有更新 v{versionStatus?.latest_version} - - )}
- {false && hasUpdate && ( - - 有更新 v{versionStatus?.latest_version} - - )}
MaiBot WebUI diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index a2659144..9a73f2df 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -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() {
- 正式版最新 + 查看主程序发布页 - {maibotStableRelease ? `v${maibotStableRelease.version}` : 'GitHub Releases'} + GitHub Releases - 测试版最新 + 查看测试版发布页 - {maibotTestRelease ? `v${maibotTestRelease.version}` : 'GitHub Releases'} + GitHub Releases - WebUI 最新 + 查看 WebUI 发布页 - v{dashboardVersionStatus?.latest_version || APP_VERSION} + PyPI diff --git a/src/main.py b/src/main.py index d7642b25..d6524885 100644 --- a/src/main.py +++ b/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: """初始化其他组件""" diff --git a/src/webui/routers/system.py b/src/webui/routers/system.py index 5c9af8ed..ddf00b31 100644 --- a/src/webui/routers/system.py +++ b/src/webui/routers/system.py @@ -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", ) diff --git a/计划.md b/计划.md index 3089daa3..317bf17f 100644 --- a/计划.md +++ b/计划.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 新入口 + - 以 `` 形式注入“这不是用户新消息,而是一次到点跟进触发” + - 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`,内容使用项目已有的 `` 风格。 -- 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` 私聊状态机 +- `☐` 主动发送后的等待态 +- `☐` 超时后再判断是否继续等、追问或闭嘴 +- `☐` 连续超时计数与打扰保护 -### 风格要求 -- 注入内容应沿用项目现有 `...` 风格 -- 它在请求里以 `user` 身份追加,但语义上明确说明“不是用户刚刚发来的新消息” -- 应提醒 planner: - - 这是一次定时跟进触发 - - 当前为什么会被唤醒 - - 你之前对用户说过什么 - - 可以主动发言,也可以不发言 - - 除非明确需要回应某条旧消息,否则不要默认调用 `reply` +### 如何做 +- 在阶段 2 的基础上,扩展主动发送后的状态记录: + - 最近一次主动发送时间 + - 是否正在等待回复 + - 等待配置 + - 连续超时计数 +- 收到用户消息时区分: + - `reply_in_time` + - `reply_late` + - `new_message` +- 等待超时后,再重新进行一轮轻量判断: + - 继续等 + - 轻追问 + - 结束等待,不再打扰 -### 提示文案打样 -```text - -以下内容不是用户刚刚发来的新消息,而是当前私聊的一次定时跟进触发。 +### 借鉴来源 +- `MoFox-Core` + - 这是最核心的借鉴来源,重点是其 `WAITING / IDLE` 与超时后再思考的设计 +- `Muice-Chatbot` + - 基本不借鉴 -触发时间:{trigger_at} +### 验收标准 +- 主动发言后,系统能进入等待态,而不是把这件事完全忘掉。 +- 用户及时回复和很久后才回复时,系统的反应能出现可区分差异。 +- 多次未回复后,系统会自动降低继续打扰的倾向。 -你之前在这个私聊中设置过一次定时跟进。 -当时记录的跟进原因: -{followup_reason} +--- -当时你已经对用户说过: -{assistant_commitment_text} +## 阶段 5:关系阶段与主线推进 -请结合当前上下文重新分析现在是否适合主动发言: -- 如果仍然适合,就继续正常分析,并在需要时调用 send_private_message。 -- 如果当前话题已经变化、用户已经提到过同类内容、约定已经失效,或者现在不适合打扰,就不要强行发送,直接 finish。 -- 不要把这段提醒当成用户的新发言。 -- 除非你明确是在回应历史中的某条具体消息,否则不要默认调用 reply。 - -``` +### 本阶段要解决的问题 +- 当前即使记住了人、能主动聊,也还没有真正的关系推进结构。 +- 缺少“朋友 → 更亲密阶段”的可持续演进逻辑。 -## 与现有链路的复用点 +### 本阶段要增加/修改的点 +- `☐` 关系阶段状态 +- `☐` 阶段推进条件 +- `☐` 近期主线事件/共同事件摘要 +- `☐` 更适合私聊恋爱化推进的上下文注入 -### 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 一轮”的入口 -- 该入口负责把 `` 注入到 planner 请求尾部 - -### 5. 主程序启动 -- 在现有后台任务体系中注册该调度器 - -## 最小验证路径 - -### 验证目标 -- 确认创建任务、到点唤醒、重新思考、主动发送、写库、上下文同步都能走通 - -### 建议路径 -1. 在私聊中让 bot 创建一条几分钟后的 `schedule_private_followup` -2. 当轮正常 `reply` 给用户,说明之后会再来跟进 -3. 到点后由调度器唤醒当前私聊的 planner -4. planner 收到 `` 后重新分析 -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. 阶段/主线推进