From ad5b5889e27d642556a68e5df57b80759992ebe6 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 6 May 2026 18:13:14 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=90=88=E5=B9=B6memory=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E4=BC=98=E5=8C=96webui=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E5=92=8C=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/components/layout/constants.ts | 3 +- dashboard/src/components/search-dialog.tsx | 4 +- .../tour/tours/model-assignment-tour.ts | 122 +-- dashboard/src/components/ui/button.tsx | 2 +- dashboard/src/components/ui/checkbox.tsx | 2 +- dashboard/src/components/ui/command.tsx | 2 +- dashboard/src/components/ui/select.tsx | 4 +- dashboard/src/components/ui/tabs.tsx | 2 +- dashboard/src/routes/config/bot.tsx | 11 +- .../config/bot/sections/FeaturesSection.tsx | 303 ------- .../src/routes/config/bot/sections/index.ts | 1 - dashboard/src/routes/config/bot/types.ts | 17 - dashboard/src/routes/config/model.tsx | 824 ++++++++++++++---- .../config/model/components/ModelCardList.tsx | 35 +- .../config/model/components/ModelTable.tsx | 38 +- .../config/model/hooks/useModelFetcher.ts | 6 +- .../routes/config/model/hooks/useModelTour.ts | 183 ++-- .../core/runtime/sdk_memory_kernel.py | 30 +- src/A_memorix/core/utils/episode_service.py | 2 +- .../core/utils/person_profile_service.py | 2 +- src/config/config.py | 6 +- src/config/legacy_migration.py | 17 + src/config/official_configs.py | 7 +- src/main.py | 9 - src/maisaka/builtin_tool/__init__.py | 2 +- src/maisaka/builtin_tool/query_memory.py | 2 +- src/services/memory_flow_service.py | 8 +- src/webui/routers/config.py | 3 - 28 files changed, 921 insertions(+), 726 deletions(-) delete mode 100644 dashboard/src/routes/config/bot/sections/FeaturesSection.tsx diff --git a/dashboard/src/components/layout/constants.ts b/dashboard/src/components/layout/constants.ts index d743a2ad..c9b29347 100644 --- a/dashboard/src/components/layout/constants.ts +++ b/dashboard/src/components/layout/constants.ts @@ -1,4 +1,4 @@ -import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, MessageSquare, Network, Package, ScrollText, Server, Settings, Sliders, Smile } from 'lucide-react' +import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, MessageSquare, Network, Package, ScrollText, Settings, Sliders, Smile } from 'lucide-react' import type { MenuSection } from './types' @@ -14,7 +14,6 @@ export const menuSections: MenuSection[] = [ title: 'sidebar.groups.botConfig', items: [ { icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot', searchDescription: 'search.items.botConfigDesc' }, - { icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', searchDescription: 'search.items.modelProviderDesc', tourId: 'sidebar-model-provider' }, { icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', searchDescription: 'search.items.modelDesc', tourId: 'sidebar-model-management' }, { icon: ScrollText, label: 'sidebar.menu.promptManagement', path: '/config/prompts' }, ], diff --git a/dashboard/src/components/search-dialog.tsx b/dashboard/src/components/search-dialog.tsx index 9de68fe4..1b29b60e 100644 --- a/dashboard/src/components/search-dialog.tsx +++ b/dashboard/src/components/search-dialog.tsx @@ -58,8 +58,8 @@ function unwrapConfigSchema(payload: unknown): ConfigSchema | null { return null } -function getModelConfigPath(fieldPath: string) { - return fieldPath.startsWith('api_providers') ? '/config/modelProvider' : '/config/model' +function getModelConfigPath(_fieldPath: string) { + return '/config/model' } function buildFieldSearchText(field: FieldSchema, fieldPath: string, sectionTitle: string, language?: string) { diff --git a/dashboard/src/components/tour/tours/model-assignment-tour.ts b/dashboard/src/components/tour/tours/model-assignment-tour.ts index f82d4cdd..85c60676 100644 --- a/dashboard/src/components/tour/tours/model-assignment-tour.ts +++ b/dashboard/src/components/tour/tours/model-assignment-tour.ts @@ -1,33 +1,31 @@ -import type { Step, Placement } from 'react-joyride' +import type { Placement, Step } from 'react-joyride' export const MODEL_ASSIGNMENT_TOUR_ID = 'model-assignment-tour' // Tour 步骤定义 export const modelAssignmentTourSteps: Step[] = [ - // Step 1: 全屏介绍 { target: 'body', - content: '本引导旨在帮助你配置模型提供商和对应的模型,并为麦麦的各个组件分配合适的模型。', + content: '本引导会帮你在同一个页面完成模型厂商、模型列表和功能分配配置。', placement: 'center' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 2: 侧边栏 - 模型提供商按钮(点击下一步会自动导航) { - target: '[data-tour="sidebar-model-provider"]', - content: '第一步,你需要配置模型提供商。模型提供商决定了你要使用谁家的模型,无论是单一厂商(如 DeepSeek),还是模型平台(如 Siliconflow),都可以在这里进行配置。点击"下一步"进入配置页面。', - placement: 'right' as Placement, + target: '[data-tour="providers-tab-trigger"]', + content: '第一步,进入"模型厂商设置"。这里用于配置要连接的模型服务厂商或模型平台。', + placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, - spotlightClicks: false, + spotlightClicks: true, + hideFooter: true, }, - // Step 3: 添加提供商按钮 { target: '[data-tour="add-provider-button"]', - content: '点击"添加提供商"按钮,开始配置你的模型提供商。', + content: '点击"添加提供商"按钮,开始配置模型厂商的连接信息。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -35,70 +33,63 @@ export const modelAssignmentTourSteps: Step[] = [ spotlightClicks: true, hideFooter: true, }, - // Step 4: 添加提供商弹窗 { target: '[data-tour="provider-dialog"]', - content: '在这里,你可以选择你想要配置的模型提供商,填写相关信息后保存即可。', + content: '在这里可以选择厂商模板,填写 API Key、URL 和连接参数,保存后即可供模型引用。', placement: 'left' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 5: 名称输入框 { target: '[data-tour="provider-name-input"]', - content: '这里的名称是你为这个模型提供商起的一个名字,方便你在后续使用时识别它。', + content: '这里的名称用于在后续模型配置中识别这个厂商。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 6: API 密钥输入框 { target: '[data-tour="provider-apikey-input"]', - content: '这里需要填写你从模型提供商那里获取的 API 密钥,用于验证和调用模型服务。对于不同的提供商,获取 API 密钥的方式可能有所不同,请参考对应提供商的文档。', + content: '这里填写从模型厂商获取的 API Key,用于验证并调用模型服务。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 7: URL 输入框 { target: '[data-tour="provider-url-input"]', - content: '这里需要填写模型提供商的 API 访问地址,确保填写正确以便系统能够连接到模型服务。对于不同的提供商,API 地址可能有所不同,请参考对应提供商的文档。', + content: '这里填写模型厂商的 API 访问地址。不同厂商或平台的地址可能不同。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 8: 模板选择下拉框 { target: '[data-tour="provider-template-select"]', - content: '当然,如果你不知道如何填写这些信息,很多模型提供商在这里都提供了预设的模板供你选择,选择对应的模板后,相关信息会自动填充。', + content: '如果不确定如何填写,可以从预设模板中选择常用厂商,相关信息会自动填充。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 9: 保存按钮 { target: '[data-tour="provider-save-button"]', - content: '填写完所有信息后,点击保存按钮,模型提供商就配置完成了。', + content: '填写完成后点击保存,模型厂商就配置好了。', placement: 'top' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 10: 取消按钮 { target: '[data-tour="provider-cancel-button"]', - content: '因为这次咱们什么都没有填写,所以点击取消按钮退出吧。', + content: '这次只是演示流程,点击取消关闭厂商配置窗口。', placement: 'top' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -106,20 +97,19 @@ export const modelAssignmentTourSteps: Step[] = [ spotlightClicks: true, hideFooter: true, }, - // Step 11: 侧边栏 - 模型管理与分配按钮(点击下一步会自动导航) { - target: '[data-tour="sidebar-model-management"]', - content: '配置好模型提供商后,接下来我们需要为麦麦添加模型并分配功能。点击"下一步"进入模型管理页面。', - placement: 'right' as Placement, - disableBeacon: true, - disableOverlayClose: true, - hideCloseButton: false, - spotlightClicks: false, - }, - // Step 12: 添加模型按钮 - { - target: '[data-tour="add-model-button"]', - content: '在为麦麦的组件分配模型之前,首先需要添加你想要分配的模型,点击"添加模型"按钮开始添加。', + target: '[data-tour="models-tab-trigger"]', + content: '厂商配置完成后,切换到"添加模型",把具体要使用的模型加入列表。', + placement: 'bottom' as Placement, + disableBeacon: true, + disableOverlayClose: true, + hideCloseButton: false, + spotlightClicks: true, + hideFooter: true, + }, + { + target: '[data-tour="add-model-button"]', + content: '点击"添加模型"按钮,开始添加一个可分配给功能的模型。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -127,60 +117,54 @@ export const modelAssignmentTourSteps: Step[] = [ spotlightClicks: true, hideFooter: true, }, - // Step 13: 添加模型弹窗 { target: '[data-tour="model-dialog"]', - content: '在这里,你可以选择你之前配置好的模型提供商,然后选择对应的模型来添加。', + content: '在这里选择刚才配置好的厂商,并填写模型名称、标识符、价格和能力参数。', placement: 'left' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 14: 模型名称输入框 { target: '[data-tour="model-name-input"]', - content: '这里的模型名称是你为这个模型起的一个名字,方便你在后续使用时识别它。', + content: '模型名称用于在任务分配时识别这个模型。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 15: API 提供商下拉框 { target: '[data-tour="model-provider-select"]', - content: '在这里选择你之前配置好的模型提供商,这样系统才能知道你要添加哪个提供商的模型。', + content: '这里选择模型所属的厂商,系统会根据厂商配置获取或调用对应模型。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 16: 模型标识符输入框 { target: '[data-tour="model-identifier-input"]', - content: '这里需要填写你想要添加的模型的标识符,不同的模型提供商可能有不同的标识符格式,请参考对应提供商的文档。', + content: '这里填写模型标识符。不同厂商的模型标识符格式可能不同,请参考对应厂商文档。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 17: 保存按钮 { target: '[data-tour="model-save-button"]', - content: '填写完所有信息后,点击保存按钮,模型就添加完成了。', + content: '填写完成后点击保存,模型就会加入可用模型列表。', placement: 'top' as Placement, disableBeacon: true, disableOverlayClose: true, hideCloseButton: false, spotlightClicks: false, }, - // Step 18: 取消按钮 { target: '[data-tour="model-cancel-button"]', - content: '当然,因为这次咱们什么都没有填写,所以直接点击取消按钮退出吧,等你准备好了再来添加模型。', + content: '这次只是演示流程,点击取消关闭模型配置窗口。', placement: 'top' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -188,10 +172,9 @@ export const modelAssignmentTourSteps: Step[] = [ spotlightClicks: true, hideFooter: true, }, - // Step 19: 为模型分配功能标签页 { target: '[data-tour="tasks-tab-trigger"]', - content: '最后一步,添加好模型后,切换到"为模型分配功能"标签页,为麦麦的各个组件分配合适的模型。', + content: '最后切换到"为模型分配功能",为麦麦的各个组件选择合适的模型。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -199,10 +182,9 @@ export const modelAssignmentTourSteps: Step[] = [ spotlightClicks: true, hideFooter: true, }, - // Step 20: 组件模型卡片的模型选择 { target: '[data-tour="task-model-select"]', - content: '在这里,你可以为每个组件选择一个或多个合适的模型,选择完成后配置会自动保存。恭喜你完成了模型配置的学习!', + content: '在这里可以为每个组件选择一个或多个模型,选择完成后配置会自动保存。', placement: 'bottom' as Placement, disableBeacon: true, disableOverlayClose: true, @@ -212,33 +194,9 @@ export const modelAssignmentTourSteps: Step[] = [ ] // 需要用户点击才能继续的步骤索引(0-based) -// Step 2 (index 2): 点击添加提供商按钮 -// Step 9 (index 9): 点击取消按钮关闭提供商弹窗 -// Step 11 (index 11): 点击添加模型按钮 -// Step 17 (index 17): 点击取消按钮关闭模型弹窗 -// Step 18 (index 18): 点击标签页切换 -export const CLICK_TO_CONTINUE_STEPS = new Set([2, 9, 11, 17, 18]) +export const CLICK_TO_CONTINUE_STEPS = new Set([1, 2, 9, 10, 11, 17, 18]) -// 步骤与路由的映射 -export const STEP_ROUTE_MAP: Record = { - 0: '/config/model', // 起始页面 - 1: '/config/model', // 侧边栏可见 - 2: '/config/modelProvider', // 需要在模型提供商页面 - 3: '/config/modelProvider', - 4: '/config/modelProvider', - 5: '/config/modelProvider', - 6: '/config/modelProvider', - 7: '/config/modelProvider', - 8: '/config/modelProvider', - 9: '/config/modelProvider', - 10: '/config/modelProvider', - 11: '/config/model', // 需要在模型管理页面 - 12: '/config/model', - 13: '/config/model', - 14: '/config/model', - 15: '/config/model', - 16: '/config/model', - 17: '/config/model', - 18: '/config/model', - 19: '/config/model', -} +// 合并后所有步骤都在模型管理与分配页面内完成 +export const STEP_ROUTE_MAP: Record = Object.fromEntries( + modelAssignmentTourSteps.map((_, index) => [index, '/config/model']) +) diff --git a/dashboard/src/components/ui/button.tsx b/dashboard/src/components/ui/button.tsx index d8f7202c..ae30672e 100644 --- a/dashboard/src/components/ui/button.tsx +++ b/dashboard/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/dashboard/src/components/ui/checkbox.tsx b/dashboard/src/components/ui/checkbox.tsx index 6da51cd6..5646ecf2 100644 --- a/dashboard/src/components/ui/checkbox.tsx +++ b/dashboard/src/components/ui/checkbox.tsx @@ -10,7 +10,7 @@ const Checkbox = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className )} {...props} @@ -117,7 +117,7 @@ const SelectItem = React.forwardRef< (null) const [expressionConfig, setExpressionConfig] = useState(null) const [emojiConfig, setEmojiConfig] = useState(null) - const [memoryConfig, setMemoryConfig] = useState(null) const [visualConfig, setVisualConfig] = useState(null) const [voiceConfig, setVoiceConfig] = useState(null) const [messageReceiveConfig, setMessageReceiveConfig] = useState(null) @@ -259,14 +258,14 @@ function BotConfigPageContent() { * 抽取自 loadConfig 和 handleModeChange 中的重复逻辑 */ const parseAndSetConfig = useCallback((config: Record) => { - configRef.current = config + const { memory: _legacyMemory, ...configWithoutLegacyMemory } = config + configRef.current = configWithoutLegacyMemory setBotConfig((config.bot ?? {}) as ConfigSectionData) setPersonalityConfig((config.personality ?? {}) as ConfigSectionData) setChatConfig((config.chat ?? {}) as ConfigSectionData) setExpressionConfig((config.expression ?? {}) as ConfigSectionData) setEmojiConfig((config.emoji ?? {}) as ConfigSectionData) - setMemoryConfig((config.memory ?? {}) as ConfigSectionData) setVisualConfig((config.visual ?? {}) as ConfigSectionData) setVoiceConfig((config.voice ?? {}) as ConfigSectionData) setMessageReceiveConfig((config.message_receive ?? {}) as ConfigSectionData) @@ -297,7 +296,6 @@ function BotConfigPageContent() { chat: chatConfig, expression: expressionConfig, emoji: emojiConfig, - memory: memoryConfig, visual: visualConfig, voice: voiceConfig, message_receive: messageReceiveConfig, @@ -321,7 +319,6 @@ function BotConfigPageContent() { chatConfig, expressionConfig, emojiConfig, - memoryConfig, visualConfig, voiceConfig, messageReceiveConfig, @@ -455,7 +452,6 @@ function BotConfigPageContent() { useConfigAutoSave(chatConfig, 'chat', initialLoadRef.current, triggerAutoSave) useConfigAutoSave(expressionConfig, 'expression', initialLoadRef.current, triggerAutoSave) useConfigAutoSave(emojiConfig, 'emoji', initialLoadRef.current, triggerAutoSave) - useConfigAutoSave(memoryConfig, 'memory', initialLoadRef.current, triggerAutoSave) useConfigAutoSave(visualConfig, 'visual', initialLoadRef.current, triggerAutoSave) useConfigAutoSave(voiceConfig, 'voice', initialLoadRef.current, triggerAutoSave) useConfigAutoSave(messageReceiveConfig, 'message_receive', initialLoadRef.current, triggerAutoSave) @@ -683,7 +679,6 @@ function BotConfigPageContent() { chat: chatConfig, expression: expressionConfig, emoji: emojiConfig, - memory: memoryConfig, visual: visualConfig, voice: voiceConfig, message_receive: messageReceiveConfig, @@ -707,7 +702,6 @@ function BotConfigPageContent() { chatConfig, expressionConfig, emojiConfig, - memoryConfig, visualConfig, voiceConfig, messageReceiveConfig, @@ -734,7 +728,6 @@ function BotConfigPageContent() { chat: setChatConfig, expression: setExpressionConfig, emoji: setEmojiConfig, - memory: setMemoryConfig, visual: setVisualConfig, voice: setVoiceConfig, message_receive: setMessageReceiveConfig, diff --git a/dashboard/src/routes/config/bot/sections/FeaturesSection.tsx b/dashboard/src/routes/config/bot/sections/FeaturesSection.tsx deleted file mode 100644 index 9bd5735d..00000000 --- a/dashboard/src/routes/config/bot/sections/FeaturesSection.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import React from 'react' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Switch } from '@/components/ui/switch' -import type { EmojiConfig, MemoryConfig, ToolConfig, VoiceConfig } from '../types' - -interface FeaturesSectionProps { - emojiConfig: EmojiConfig - memoryConfig: MemoryConfig - toolConfig: ToolConfig - voiceConfig: VoiceConfig - onEmojiChange: (config: EmojiConfig) => void - onMemoryChange: (config: MemoryConfig) => void - onToolChange: (config: ToolConfig) => void - onVoiceChange: (config: VoiceConfig) => void -} - -export const FeaturesSection = React.memo(function FeaturesSection({ - emojiConfig, - memoryConfig, - toolConfig, - voiceConfig, - onEmojiChange, - onMemoryChange, - onToolChange, - onVoiceChange, -}: FeaturesSectionProps) { - return ( -
- {/* 工具设置 */} -
-
-

工具设置

-
-
- onToolChange({ ...toolConfig, enable_tool: checked })} - /> - -
-

- 允许麦麦使用各种工具来增强功能 -

- -
- onVoiceChange({ ...voiceConfig, enable_asr: checked })} - /> - -
-

- 启用后麦麦可以识别语音消息,需要配置语音识别模型 -

-
-
-
- - {/* 记忆设置 */} -
-
-

记忆设置

-
-
- - - onMemoryChange({ ...memoryConfig, max_agent_iterations: parseInt(e.target.value) }) - } - /> -

最低为 1(不深入思考)

-
- -
- - - onMemoryChange({ ...memoryConfig, agent_timeout_seconds: parseFloat(e.target.value) }) - } - /> -

记忆检索的超时时间,避免过长的等待

-
- -
- - onMemoryChange({ ...memoryConfig, enable_jargon_detection: checked }) - } - /> - -
-

- 记忆检索过程中是否启用黑话识别 -

- - {/* 聊天历史总结配置 */} -
-

聊天历史总结配置

-
-
- - - onMemoryChange({ ...memoryConfig, chat_history_topic_check_message_threshold: parseInt(e.target.value) }) - } - /> -

- 当累积消息数达到此值时触发话题检查 -

-
- -
- - - onMemoryChange({ ...memoryConfig, chat_history_topic_check_time_hours: parseFloat(e.target.value) }) - } - /> -

- 当距离上次检查超过此时间且消息数达到最小阈值时触发话题检查 -

-
- -
- - - onMemoryChange({ ...memoryConfig, chat_history_topic_check_min_messages: parseInt(e.target.value) }) - } - /> -

- 时间触发模式下的最小消息数阈值 -

-
- -
- - - onMemoryChange({ ...memoryConfig, chat_history_finalize_no_update_checks: parseInt(e.target.value) }) - } - /> -

- 当话题连续N次检查无新增内容时触发打包存储 -

-
- -
- - - onMemoryChange({ ...memoryConfig, chat_history_finalize_message_count: parseInt(e.target.value) }) - } - /> -

- 当话题的消息条数超过此值时触发打包存储 -

-
-
-
-
-
-
- - {/* 表情包设置 */} -
-
-

表情包设置

-
-
- - - onEmojiChange({ ...emojiConfig, emoji_chance: parseFloat(e.target.value) }) - } - /> -

范围 0-1,越大越容易发送表情包

-
- -
- - - onEmojiChange({ ...emojiConfig, max_reg_num: parseInt(e.target.value) }) - } - /> -

麦麦最多可以注册的表情包数量

-
- -
- - - onEmojiChange({ ...emojiConfig, check_interval: parseInt(e.target.value) }) - } - /> -

- 检查表情包(注册、破损、删除)的时间间隔 -

-
- -
- - onEmojiChange({ ...emojiConfig, do_replace: checked }) - } - /> - -
- -
- - onEmojiChange({ ...emojiConfig, steal_emoji: checked }) - } - /> - -
-

- 允许麦麦将看到的表情包据为己有 -

- -
- - onEmojiChange({ ...emojiConfig, content_filtration: checked }) - } - /> - -
-
-
-
-
- ) -}) diff --git a/dashboard/src/routes/config/bot/sections/index.ts b/dashboard/src/routes/config/bot/sections/index.ts index 97fd7e3e..ac75d726 100644 --- a/dashboard/src/routes/config/bot/sections/index.ts +++ b/dashboard/src/routes/config/bot/sections/index.ts @@ -10,7 +10,6 @@ export { LogSection } from './LogSection' export { DebugSection } from './DebugSection' export { MaimMessageSection } from './MaimMessageSection' export { TelemetrySection } from './TelemetrySection' -export { FeaturesSection } from './FeaturesSection' export { ExpressionSection } from './ExpressionSection' export { ProcessingSection } from './ProcessingSection' export { default as MessageReceiveSection } from './MessageReceiveSection' diff --git a/dashboard/src/routes/config/bot/types.ts b/dashboard/src/routes/config/bot/types.ts index 4c92bd5a..4a39bd30 100644 --- a/dashboard/src/routes/config/bot/types.ts +++ b/dashboard/src/routes/config/bot/types.ts @@ -80,21 +80,6 @@ export interface EmojiConfig { content_filtration: boolean } -export interface MemoryConfig { - max_agent_iterations: number - agent_timeout_seconds: number - enable_jargon_detection: boolean - chat_history_topic_check_message_threshold: number - chat_history_topic_check_time_hours: number - chat_history_topic_check_min_messages: number - chat_history_finalize_no_update_checks: number - chat_history_finalize_message_count: number -} - -export interface ToolConfig { - enable_tool: boolean -} - // MoodConfig 已在后端移除 export interface VoiceConfig { @@ -226,8 +211,6 @@ export interface AllBotConfigs { chatConfig: ChatConfig | null expressionConfig: ExpressionConfig | null emojiConfig: EmojiConfig | null - memoryConfig: MemoryConfig | null - toolConfig: ToolConfig | null voiceConfig: VoiceConfig | null messageReceiveConfig: MessageReceiveConfig | null dreamConfig: DreamConfig | null diff --git a/dashboard/src/routes/config/model.tsx b/dashboard/src/routes/config/model.tsx index 01fd801d..15012f48 100644 --- a/dashboard/src/routes/config/model.tsx +++ b/dashboard/src/routes/config/model.tsx @@ -48,8 +48,9 @@ import { import { Switch } from '@/components/ui/switch' import { Slider } from '@/components/ui/slider' import { Badge } from '@/components/ui/badge' -import { Plus, Pencil, Trash2, Save, Search, Info, Power, Check, ChevronsUpDown, RefreshCw, Loader2, GraduationCap, Share2, AlertTriangle, Settings } from 'lucide-react' -import { getModelConfig, getModelConfigSchema, updateModelConfig } from '@/lib/config-api' +import { Plus, Trash2, Save, Search, Info, Power, Check, ChevronsUpDown, RefreshCw, Loader2, GraduationCap, Share2, AlertTriangle, Settings, Zap } from 'lucide-react' +import { getModelConfig, getModelConfigSchema, testProviderConnection, updateModelConfig, updateModelConfigSection } from '@/lib/config-api' +import type { TestConnectionResult } from '@/lib/config-api' import { resolveFieldLabel } from '@/lib/config-label' import type { ConfigSchema } from '@/types/config-schema' import { useToast } from '@/hooks/use-toast' @@ -59,6 +60,12 @@ import { RestartOverlay } from '@/components/restart-overlay' import { RestartProvider, useRestart } from '@/lib/restart-context' import { ExtraParamsDialog } from '@/components/ui/extra-params-dialog' import { SharePackDialog } from '@/components/share-pack-dialog' +import { TaskConfigCard, Pagination, ModelTable, ModelCardList } from './model/components' +import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks' +import { ProviderForm } from './modelProvider/ProviderForm' +import { ProviderList } from './modelProvider/ProviderList' +import type { APIProvider, DeleteConfirmState } from './modelProvider/types' +import { cleanProviderData } from './modelProvider/utils' // 导入模块化的类型定义和组件 import type { ModelInfo, ProviderConfig, ModelTaskConfig, TaskConfig } from './model/types' @@ -70,8 +77,6 @@ function unwrapModelConfig(data: unknown): Record { } return data as Record } -import { TaskConfigCard, Pagination, ModelTable, ModelCardList } from './model/components' -import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks' // 主导出组件:包装 RestartProvider export function ModelConfigPage() { @@ -88,6 +93,7 @@ function ModelConfigPageContent() { const [models, setModels] = useState([]) const [providers, setProviders] = useState([]) const [providerConfigs, setProviderConfigs] = useState([]) + const [apiProviders, setApiProviders] = useState([]) const [modelNames, setModelNames] = useState([]) const [taskConfig, setTaskConfig] = useState(null) const [loading, setLoading] = useState(true) @@ -100,13 +106,31 @@ function ModelConfigPageContent() { const [extraParamsDialogOpen, setExtraParamsDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deletingIndex, setDeletingIndex] = useState(null) + const [providerDialogOpen, setProviderDialogOpen] = useState(false) + const [editingProvider, setEditingProvider] = useState(null) + const [editingProviderIndex, setEditingProviderIndex] = useState(null) + const [providerDeleteDialogOpen, setProviderDeleteDialogOpen] = useState(false) + const [deletingProviderIndex, setDeletingProviderIndex] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [selectedModels, setSelectedModels] = useState>(new Set()) + const [selectedProviders, setSelectedProviders] = useState>(new Set()) const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false) + const [providerBatchDeleteDialogOpen, setProviderBatchDeleteDialogOpen] = useState(false) + const [testingProviders, setTestingProviders] = useState>(new Set()) + const [testResults, setTestResults] = useState>(new Map()) + const [deleteConfirmState, setDeleteConfirmState] = useState({ + isOpen: false, + providersToDelete: [], + affectedModels: [], + pendingProviders: [], + context: 'auto', + oldProviders: [], + }) const [taskConfigSchema, setTaskConfigSchema] = useState(null) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [jumpToPage, setJumpToPage] = useState('') + const [activeTab, setActiveTab] = useState('providers') const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false) const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false) @@ -119,6 +143,8 @@ function ModelConfigPageContent() { // 模型 Combobox 状态 const [modelComboboxOpen, setModelComboboxOpen] = useState(false) + const providerAutoSaveTimerRef = useRef | null>(null) + const providersSnapshotRef = useRef(null) // 嵌入模型警告相关状态 const [embeddingWarningOpen, setEmbeddingWarningOpen] = useState(false) @@ -199,6 +225,8 @@ function ModelConfigPageContent() { const providerList = (config.api_providers as ProviderConfig[]) || [] setProviders(providerList.map((p) => p.name)) setProviderConfigs(providerList) + setApiProviders(providerList.map((provider) => cleanProviderData(provider as APIProvider))) + providersSnapshotRef.current = JSON.stringify(providerList.map((provider) => cleanProviderData(provider as APIProvider))) const taskConf = (config.model_task_config as ModelTaskConfig) || null setTaskConfig(taskConf) @@ -267,6 +295,167 @@ function ModelConfigPageContent() { localStorage.setItem('model-assignment-tour-entry-dismissed', 'true') setTourEntryVisible(false) } + + const syncProviderState = useCallback((nextProviders: APIProvider[]) => { + const cleanedProviders = nextProviders.map(cleanProviderData) + setApiProviders(cleanedProviders) + setProviders(cleanedProviders.map((provider) => provider.name)) + setProviderConfigs(cleanedProviders.map((provider) => ({ + name: provider.name, + base_url: provider.base_url, + api_key: provider.api_key, + client_type: provider.client_type, + max_retry: provider.max_retry ?? 2, + timeout: provider.timeout ?? 30, + retry_interval: provider.retry_interval ?? 10, + }))) + }, []) + + const removeModelsForProviders = useCallback(( + sourceModels: ModelInfo[], + sourceTaskConfig: ModelTaskConfig | null, + removedModels: unknown[], + ) => { + const removedModelNames = new Set( + removedModels + .map((model) => (typeof model === 'object' && model !== null && 'name' in model ? String((model as Record).name) : '')) + .filter(Boolean) + ) + if (removedModelNames.size === 0) { + return { models: sourceModels, taskConfig: sourceTaskConfig } + } + + const nextModels = sourceModels.filter((model) => !removedModelNames.has(model.name)) + if (!sourceTaskConfig) { + return { models: nextModels, taskConfig: sourceTaskConfig } + } + + const nextTaskConfig: ModelTaskConfig = {} + for (const [taskName, task] of Object.entries(sourceTaskConfig)) { + nextTaskConfig[taskName] = { + ...task, + model_list: (task?.model_list || []).filter((modelName) => !removedModelNames.has(modelName)), + } + } + return { models: nextModels, taskConfig: nextTaskConfig } + }, []) + + const checkDeleteProviderImpact = useCallback(async ( + nextProviders: APIProvider[], + context: 'auto' | 'manual' | 'restart' = 'auto' + ) => { + const oldProviderNames = new Set(apiProviders.map((provider) => provider.name)) + const nextProviderNames = new Set(nextProviders.map((provider) => provider.name)) + const deletedProviders = Array.from(oldProviderNames).filter((name) => !nextProviderNames.has(name)) + + if (deletedProviders.length === 0) { + return { shouldProceed: true } + } + + const affectedModels = models.filter((model) => deletedProviders.includes(model.api_provider)) + if (affectedModels.length === 0) { + return { shouldProceed: true } + } + + setDeleteConfirmState({ + isOpen: true, + providersToDelete: deletedProviders, + affectedModels, + pendingProviders: nextProviders, + context, + oldProviders: [...apiProviders], + }) + return { shouldProceed: false } + }, [apiProviders, models]) + + const saveProviders = useCallback(async ( + nextProviders: APIProvider[], + context: 'auto' | 'manual' | 'restart' = 'auto', + affectedModels: unknown[] = [] + ) => { + const cleanedProviders = nextProviders.map(cleanProviderData) + const { models: nextModels, taskConfig: nextTaskConfig } = removeModelsForProviders(models, taskConfig, affectedModels) + + if (context === 'auto' && affectedModels.length === 0) { + const result = await updateModelConfigSection('api_providers', cleanedProviders) + if (!result.success) { + throw new Error(result.error || '保存提供商失败') + } + } else { + const resultGet = await getModelConfig() + if (!resultGet.success) { + throw new Error(resultGet.error || '加载模型配置失败') + } + const config = unwrapModelConfig(resultGet.data) + config.api_providers = cleanedProviders + config.models = nextModels.map(cleanModelForSave) + config.model_task_config = nextTaskConfig + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + throw new Error(resultUpdate.error || '保存模型配置失败') + } + } + + syncProviderState(cleanedProviders) + setModels(nextModels) + setModelNames(nextModels.map((model) => model.name)) + setTaskConfig(nextTaskConfig) + checkTaskConfigIssues(nextTaskConfig, nextModels) + providersSnapshotRef.current = JSON.stringify(cleanedProviders) + setHasUnsavedChanges(false) + + if (context === 'restart') { + await handleRestart() + } + }, [checkTaskConfigIssues, models, removeModelsForProviders, syncProviderState, taskConfig]) + + const autoSaveProviders = useCallback(async (nextProviders: APIProvider[]) => { + if (initialLoadRef.current) return + const { shouldProceed } = await checkDeleteProviderImpact(nextProviders, 'auto') + if (!shouldProceed) { + setHasUnsavedChanges(true) + return + } + + try { + setAutoSaving(true) + await saveProviders(nextProviders, 'auto') + } catch (error) { + console.error('自动保存提供商失败:', error) + toast({ + title: '自动保存失败', + description: (error as Error).message, + variant: 'destructive', + }) + setHasUnsavedChanges(true) + } finally { + setAutoSaving(false) + } + }, [checkDeleteProviderImpact, initialLoadRef, saveProviders, toast]) + + useEffect(() => { + if (initialLoadRef.current) return + const snapshot = JSON.stringify(apiProviders.map(cleanProviderData)) + if (providersSnapshotRef.current === null) { + providersSnapshotRef.current = snapshot + return + } + if (snapshot === providersSnapshotRef.current) return + + setHasUnsavedChanges(true) + if (providerAutoSaveTimerRef.current) { + clearTimeout(providerAutoSaveTimerRef.current) + } + providerAutoSaveTimerRef.current = setTimeout(() => { + autoSaveProviders(apiProviders) + }, 2000) + + return () => { + if (providerAutoSaveTimerRef.current) { + clearTimeout(providerAutoSaveTimerRef.current) + } + } + }, [apiProviders, autoSaveProviders, initialLoadRef]) // 一键删除所有无效模型引用 const handleRemoveInvalidRefs = useCallback(() => { @@ -322,6 +511,9 @@ function ModelConfigPageContent() { try { setSaving(true) clearAutoSaveTimers() + if (providerAutoSaveTimerRef.current) { + clearTimeout(providerAutoSaveTimerRef.current) + } const resultGet = await getModelConfig() if (!resultGet.success) { toast({ @@ -334,6 +526,7 @@ function ModelConfigPageContent() { } const config = unwrapModelConfig(resultGet.data) // 清理每个模型中的 null 值 + config.api_providers = apiProviders.map(cleanProviderData) config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig const resultUpdate = await updateModelConfig(config) @@ -347,6 +540,7 @@ function ModelConfigPageContent() { return } resetSnapshots(config.models as ModelInfo[], taskConfig) + providersSnapshotRef.current = JSON.stringify(config.api_providers) setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -371,6 +565,9 @@ function ModelConfigPageContent() { // 先取消自动保存定时器 clearAutoSaveTimers() + if (providerAutoSaveTimerRef.current) { + clearTimeout(providerAutoSaveTimerRef.current) + } const resultGet = await getModelConfig() if (!resultGet.success) { @@ -384,6 +581,7 @@ function ModelConfigPageContent() { } const config = unwrapModelConfig(resultGet.data) // 清理每个模型中的 null 值 + config.api_providers = apiProviders.map(cleanProviderData) config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig const resultUpdate = await updateModelConfig(config) @@ -397,6 +595,7 @@ function ModelConfigPageContent() { return } resetSnapshots(config.models as ModelInfo[], taskConfig) + providersSnapshotRef.current = JSON.stringify(config.api_providers) setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -441,12 +640,49 @@ function ModelConfigPageContent() { setEditDialogOpen(true) } + const openProviderDialog = (provider: APIProvider | null, index: number | null) => { + setEditingProvider(provider || { + name: '', + base_url: '', + api_key: '', + client_type: 'openai', + max_retry: 2, + timeout: 30, + retry_interval: 10, + }) + setEditingProviderIndex(index) + setProviderDialogOpen(true) + } + // Tour 引导 (使用 hook 封装的逻辑) const { startTour: handleStartTour, isRunning: tourIsRunning } = useModelTour({ onOpenEditDialog: () => openEditDialog(null, null), onCloseEditDialog: () => setEditDialogOpen(false), + onOpenProviderDialog: () => openProviderDialog(null, null), + onCloseProviderDialog: () => setProviderDialogOpen(false), + onOpenProvidersTab: () => setActiveTab('providers'), + onOpenModelsTab: () => setActiveTab('models'), + onOpenTasksTab: () => setActiveTab('tasks'), }) + const handleSaveProviderEdit = (provider: APIProvider, index: number | null) => { + const providerToSave = cleanProviderData(provider) + if (index !== null) { + const nextProviders = [...apiProviders] + nextProviders[index] = providerToSave + syncProviderState(nextProviders) + } else { + syncProviderState([...apiProviders, providerToSave]) + } + setProviderDialogOpen(false) + setEditingProvider(null) + setEditingProviderIndex(null) + toast({ + title: index !== null ? '提供商已更新' : '提供商已添加', + description: '配置将在 2 秒后自动保存,或点击右上角"保存配置"按钮立即保存', + }) + } + // 保存编辑 const handleSaveEdit = () => { if (!editingModel) return @@ -581,6 +817,168 @@ function ModelConfigPageContent() { setDeletingIndex(null) } + const openProviderDeleteDialog = (index: number) => { + setDeletingProviderIndex(index) + setProviderDeleteDialogOpen(true) + } + + const handleConfirmProviderDelete = async () => { + if (deletingProviderIndex !== null) { + const nextProviders = apiProviders.filter((_, index) => index !== deletingProviderIndex) + const { shouldProceed } = await checkDeleteProviderImpact(nextProviders, 'manual') + if (shouldProceed) { + syncProviderState(nextProviders) + toast({ + title: '删除成功', + description: '提供商已从列表中移除', + }) + } + } + setProviderDeleteDialogOpen(false) + setDeletingProviderIndex(null) + } + + const toggleProviderSelection = (index: number) => { + const nextSelected = new Set(selectedProviders) + if (nextSelected.has(index)) { + nextSelected.delete(index) + } else { + nextSelected.add(index) + } + setSelectedProviders(nextSelected) + } + + const toggleSelectAllProviders = () => { + if (selectedProviders.size === apiProviders.length) { + setSelectedProviders(new Set()) + } else { + setSelectedProviders(new Set(apiProviders.map((_, index) => index))) + } + } + + const openProviderBatchDeleteDialog = () => { + if (selectedProviders.size === 0) { + toast({ + title: '提示', + description: '请先选择要删除的提供商', + variant: 'default', + }) + return + } + setProviderBatchDeleteDialogOpen(true) + } + + const handleConfirmProviderBatchDelete = async () => { + const nextProviders = apiProviders.filter((_, index) => !selectedProviders.has(index)) + const { shouldProceed } = await checkDeleteProviderImpact(nextProviders, 'manual') + if (shouldProceed) { + const deletedCount = selectedProviders.size + syncProviderState(nextProviders) + setSelectedProviders(new Set()) + toast({ + title: '批量删除成功', + description: `已删除 ${deletedCount} 个提供商`, + }) + } + setProviderBatchDeleteDialogOpen(false) + } + + const handleConfirmDeleteProviderImpact = async () => { + try { + const savingFlag = deleteConfirmState.context === 'auto' ? setAutoSaving : setSaving + savingFlag(true) + await saveProviders( + deleteConfirmState.pendingProviders, + deleteConfirmState.context, + deleteConfirmState.affectedModels + ) + toast({ + title: '删除成功', + description: `已删除 ${deleteConfirmState.providersToDelete.length} 个提供商和 ${deleteConfirmState.affectedModels.length} 个关联模型`, + }) + setDeleteConfirmState({ + isOpen: false, + providersToDelete: [], + affectedModels: [], + pendingProviders: [], + context: 'auto', + oldProviders: [], + }) + setSelectedProviders(new Set()) + } catch (error) { + toast({ + title: '删除失败', + description: (error as Error).message, + variant: 'destructive', + }) + } finally { + setSaving(false) + setAutoSaving(false) + } + } + + const handleCancelDeleteProviderImpact = () => { + if (deleteConfirmState.oldProviders.length > 0) { + syncProviderState(deleteConfirmState.oldProviders) + } + setDeleteConfirmState({ + isOpen: false, + providersToDelete: [], + affectedModels: [], + pendingProviders: [], + context: 'auto', + oldProviders: [], + }) + setHasUnsavedChanges(false) + } + + const handleTestProviderConnection = async (providerName: string) => { + setTestingProviders((prev) => new Set(prev).add(providerName)) + try { + const result = await testProviderConnection(providerName) + if (!result.success) { + toast({ + title: '测试失败', + description: result.error, + variant: 'destructive', + }) + return + } + const testResult = result.data + setTestResults((prev) => new Map(prev).set(providerName, testResult)) + if (testResult.network_ok && testResult.api_key_valid !== false) { + toast({ + title: testResult.api_key_valid === true ? '连接正常' : '网络连接正常', + description: `${providerName} 可以访问 (${testResult.latency_ms}ms)`, + }) + } else { + toast({ + title: testResult.network_ok ? '连接正常但 Key 无效' : '连接失败', + description: testResult.error || `${providerName} API Key 无效或无法连接`, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '测试失败', + description: (error as Error).message, + variant: 'destructive', + }) + } finally { + setTestingProviders((prev) => { + const next = new Set(prev) + next.delete(providerName) + return next + }) + } + } + + const handleTestAllProviderConnections = async () => { + for (const provider of apiProviders) { + await handleTestProviderConnection(provider.name) + } + } + // 切换单个模型选择 const toggleModelSelection = (index: number) => { const newSelected = new Set(selectedModels) @@ -902,11 +1300,59 @@ function ModelConfigPageContent() { )} {/* 标签页 */} - - - 添加模型 + + + 模型厂商设置 + 添加模型 为模型分配功能 + {/* 模型厂商设置标签页 */} + +
+

+ 管理 AI 模型厂商的 API 配置 +

+
+ {selectedProviders.size > 0 && ( + + )} + + +
+
+ + +
{/* 模型配置标签页 */}
@@ -1030,6 +1476,104 @@ function ModelConfigPageContent() { + + + {/* 删除提供商确认对话框 */} + + + + 确认删除提供商 + + 确定要删除提供商"{deletingProviderIndex !== null ? apiProviders[deletingProviderIndex]?.name : ''}"吗? + 如果该提供商下存在模型,确认时会提示一并处理关联模型。 + + + + 取消 + + 删除 + + + + + + {/* 批量删除提供商确认对话框 */} + + + + 确认批量删除提供商 + + 确定要删除选中的 {selectedProviders.size} 个提供商吗? + 如果这些提供商下存在模型,确认时会提示一并处理关联模型。 + + + + 取消 + + 批量删除 + + + + + + {/* 删除提供商影响确认对话框 */} + + + + + + 删除提供商会同时移除关联模型 + + +
+

+ 将删除 {deleteConfirmState.providersToDelete.length} 个提供商,并移除 + {' '}{deleteConfirmState.affectedModels.length} 个使用这些提供商的模型。 +

+ {deleteConfirmState.affectedModels.length > 0 && ( +
+ {deleteConfirmState.affectedModels.slice(0, 8).map((model) => ( +
+ {(model as ModelInfo).name} ({(model as ModelInfo).api_provider}) +
+ ))} + {deleteConfirmState.affectedModels.length > 8 && ( +
还有 {deleteConfirmState.affectedModels.length - 8} 个模型...
+ )} +
+ )} +

+ 关联模型会从模型列表和任务分配中移除,此操作无法撤销。 +

+
+
+
+ + 取消 + + 确认删除 + + +
+
+ {/* 编辑模型对话框 */} {formErrors.name ? (

{formErrors.name}

- ) : ( -

- 用于在任务配置中引用此模型 -

- )} + ) : null}
@@ -1153,99 +1693,89 @@ function ModelConfigPageContent() { )}
- {/* 模型标识符 Combobox */} - {matchedTemplate?.modelFetcher ? ( - - - - - - - - - - - {modelFetchError ? ( -
-

{modelFetchError}

- {!modelFetchError.includes('API Key') && ( - - )} -
- ) : ( - '未找到匹配的模型' - )} -
- - {availableModels.map((model) => ( - { - setEditingModel((prev) => - prev ? { ...prev, model_identifier: model.id } : null - ) - setModelComboboxOpen(false) - }} - > - -
- {model.id} - {model.name !== model.id && ( - {model.name} +
+ {/* 模型标识符 Combobox */} + {matchedTemplate?.modelFetcher && ( + + + + + + + + + + + {modelFetchError ? ( +
+

{modelFetchError}

+ {!modelFetchError.includes('API Key') && ( + )}
- - ))} - - - { - setModelComboboxOpen(false) - // 聚焦到手动输入框(如果需要的话可以实现) - }} - > - - 手动输入模型标识符... - - -
-
-
-
-
- ) : ( + ) : ( + '未找到匹配的模型' + )} + + + {availableModels.map((model) => ( + { + setEditingModel((prev) => + prev ? { ...prev, model_identifier: model.id } : null + ) + setModelComboboxOpen(false) + }} + > + +
+ {model.id} + {model.name !== model.id && ( + {model.name} + )} +
+
+ ))} +
+ + + + + + )} + ({ ...prev, model_identifier: undefined })) } }} - placeholder="Qwen/Qwen3-30B-A3B-Instruct-2507" - className={formErrors.model_identifier ? 'border-destructive focus-visible:ring-destructive' : ''} + placeholder={matchedTemplate?.modelFetcher ? '手动输入模型标识符' : 'Qwen/Qwen3-30B-A3B-Instruct-2507'} + className={`${matchedTemplate?.modelFetcher ? 'sm:flex-1' : 'w-full'} ${formErrors.model_identifier ? 'border-destructive focus-visible:ring-destructive' : ''}`} /> - )} +
{/* 表单验证错误提示 */} {formErrors.model_identifier && ( @@ -1277,27 +1807,10 @@ function ModelConfigPageContent() { )} - {/* 手动输入区域 - 当使用 Combobox 时也显示一个可编辑的输入框 */} - {matchedTemplate?.modelFetcher && ( - { - setEditingModel((prev) => - prev ? { ...prev, model_identifier: e.target.value } : null - ) - if (formErrors.model_identifier) { - setFormErrors((prev) => ({ ...prev, model_identifier: undefined })) - } - }} - placeholder="或手动输入模型标识符" - className={`mt-2 ${formErrors.model_identifier ? 'border-destructive focus-visible:ring-destructive' : ''}`} - /> - )} - {!formErrors.model_identifier && (

{modelFetchError - ? '请手动输入模型标识符,或前往"模型提供商配置"检查 API Key' + ? '请手动输入模型标识符,或前往"模型厂商设置"检查 API Key' : matchedTemplate?.modelFetcher ? `已识别为 ${matchedTemplate.display_name},支持自动获取模型列表` : 'API 提供商提供的模型 ID'} @@ -1305,6 +1818,21 @@ function ModelConfigPageContent() { )}

+
+ + setEditingModel((prev) => + prev ? { ...prev, visual: checked } : null + ) + } + /> + +
+
@@ -1388,6 +1916,24 @@ function ModelConfigPageContent() { />
)} + +
+
+ +

+ 用于必须通过流式响应返回内容的模型 +

+
+ + setEditingModel((prev) => + prev ? { ...prev, force_stream_mode: checked } : null + ) + } + /> +
)} @@ -1555,36 +2101,6 @@ function ModelConfigPageContent() { )} -
- - setEditingModel((prev) => - prev ? { ...prev, visual: checked } : null - ) - } - /> - -
- -
- - setEditingModel((prev) => - prev ? { ...prev, force_stream_mode: checked } : null - ) - } - /> - -
- {/* 额外参数 */}
diff --git a/dashboard/src/routes/config/model/components/ModelCardList.tsx b/dashboard/src/routes/config/model/components/ModelCardList.tsx index 2031642b..de6fb94f 100644 --- a/dashboard/src/routes/config/model/components/ModelCardList.tsx +++ b/dashboard/src/routes/config/model/components/ModelCardList.tsx @@ -3,7 +3,6 @@ */ import React from 'react' import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Pencil, Trash2 } from 'lucide-react' import type { ModelInfo } from '../types' @@ -49,17 +48,15 @@ export const ModelCardList = React.memo(function ModelCardList({

{model.name}

- - {used ? '已使用' : '未使用'} - - {model.visual && ( - - 视觉 - - )} +

{model.model_identifier} @@ -90,8 +87,18 @@ export const ModelCardList = React.memo(function ModelCardList({

{model.api_provider}

- 模型温度 -

{model.temperature != null ? model.temperature : 默认}

+ 视觉 +

+ +

输入价格 diff --git a/dashboard/src/routes/config/model/components/ModelTable.tsx b/dashboard/src/routes/config/model/components/ModelTable.tsx index 305d6f29..99e7792c 100644 --- a/dashboard/src/routes/config/model/components/ModelTable.tsx +++ b/dashboard/src/routes/config/model/components/ModelTable.tsx @@ -3,7 +3,6 @@ */ import React from 'react' import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Table, @@ -63,11 +62,11 @@ export const ModelTable = React.memo(function ModelTable({ onCheckedChange={onToggleSelectAll} /> - 使用状态 + 使用 模型名称 模型标识符 提供商 - 视觉 + 视觉 温度 输入价格 输出价格 @@ -93,13 +92,16 @@ export const ModelTable = React.memo(function ModelTable({ onCheckedChange={() => onToggleSelection(actualIndex)} /> - - - {used ? '已使用' : '未使用'} - + + {model.name} @@ -107,13 +109,15 @@ export const ModelTable = React.memo(function ModelTable({ {model.api_provider} - {model.visual ? ( - - 启用 - - ) : ( - - - )} + {model.temperature != null ? model.temperature : -} diff --git a/dashboard/src/routes/config/model/hooks/useModelFetcher.ts b/dashboard/src/routes/config/model/hooks/useModelFetcher.ts index 073c6bfa..18212942 100644 --- a/dashboard/src/routes/config/model/hooks/useModelFetcher.ts +++ b/dashboard/src/routes/config/model/hooks/useModelFetcher.ts @@ -51,7 +51,7 @@ export function useModelFetcher(options: UseModelFetcherOptions): UseModelFetche if (!config?.base_url) { setAvailableModels([]) setMatchedTemplate(null) - setModelFetchError('提供商配置不完整,请先在"模型提供商配置"中配置') + setModelFetchError('提供商配置不完整,请先在"模型厂商设置"中配置') return } @@ -59,7 +59,7 @@ export function useModelFetcher(options: UseModelFetcherOptions): UseModelFetche if (!config.api_key) { setAvailableModels([]) setMatchedTemplate(null) - setModelFetchError('该提供商未配置 API Key,请先在"模型提供商配置"中填写') + setModelFetchError('该提供商未配置 API Key,请先在"模型厂商设置"中填写') return } @@ -105,7 +105,7 @@ export function useModelFetcher(options: UseModelFetcherOptions): UseModelFetche const errorMessage = (error as Error).message || '获取模型列表失败' // 根据错误类型提供更友好的提示 if (errorMessage.includes('无效') || errorMessage.includes('过期') || errorMessage.includes('API Key')) { - setModelFetchError('API Key 无效或已过期,请检查"模型提供商配置"中的密钥') + setModelFetchError('API Key 无效或已过期,请检查"模型厂商设置"中的密钥') } else if (errorMessage.includes('权限')) { setModelFetchError('没有权限获取模型列表,请检查 API Key 权限') } else if (errorMessage.includes('timeout') || errorMessage.includes('超时')) { diff --git a/dashboard/src/routes/config/model/hooks/useModelTour.ts b/dashboard/src/routes/config/model/hooks/useModelTour.ts index 01ae3b34..246fe7c6 100644 --- a/dashboard/src/routes/config/model/hooks/useModelTour.ts +++ b/dashboard/src/routes/config/model/hooks/useModelTour.ts @@ -1,16 +1,27 @@ /** - * Model 配置页面 Tour 引导 Hook + * 模型配置页面 Tour 引导 Hook */ -import { useEffect, useRef, useCallback } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { useNavigate } from '@tanstack/react-router' + import { useTour } from '@/components/tour' -import { MODEL_ASSIGNMENT_TOUR_ID, modelAssignmentTourSteps, STEP_ROUTE_MAP } from '@/components/tour/tours/model-assignment-tour' +import { MODEL_ASSIGNMENT_TOUR_ID, STEP_ROUTE_MAP, modelAssignmentTourSteps } from '@/components/tour/tours/model-assignment-tour' interface UseModelTourOptions { /** 打开模型编辑对话框回调 */ onOpenEditDialog?: () => void - /** 关闭编辑对话框回调 */ + /** 关闭模型编辑对话框回调 */ onCloseEditDialog?: () => void + /** 打开提供商编辑对话框回调 */ + onOpenProviderDialog?: () => void + /** 关闭提供商编辑对话框回调 */ + onCloseProviderDialog?: () => void + /** 切换到模型厂商设置标签页 */ + onOpenProvidersTab?: () => void + /** 切换到添加模型标签页 */ + onOpenModelsTab?: () => void + /** 切换到模型分配标签页 */ + onOpenTasksTab?: () => void } interface UseModelTourReturn { @@ -22,15 +33,18 @@ interface UseModelTourReturn { stepIndex: number } -/** - * Model 配置页面 Tour 引导 Hook - */ export function useModelTour(options: UseModelTourOptions = {}): UseModelTourReturn { - const { onOpenEditDialog, onCloseEditDialog } = options + const { + onOpenEditDialog, + onCloseEditDialog, + onOpenProviderDialog, + onCloseProviderDialog, + onOpenProvidersTab, + onOpenModelsTab, + onOpenTasksTab, + } = options const navigate = useNavigate() const { registerTour, startTour: startTourFn, state: tourState, goToStep } = useTour() - - // 用于追踪前一个步骤 const prevTourStepRef = useRef(tourState.stepIndex) const didClickTourTarget = useCallback((event: MouseEvent, selector: string) => { @@ -53,100 +67,121 @@ export function useModelTour(options: UseModelTourOptions = {}): UseModelTourRet ) }, []) - // 注册 Tour useEffect(() => { registerTour(MODEL_ASSIGNMENT_TOUR_ID, modelAssignmentTourSteps) }, [registerTour]) - // 监听 Tour 步骤变化,处理页面导航 useEffect(() => { - if (tourState.activeTourId === MODEL_ASSIGNMENT_TOUR_ID && tourState.isRunning) { - const targetRoute = STEP_ROUTE_MAP[tourState.stepIndex] - if (targetRoute && !window.location.pathname.endsWith(targetRoute.replace('/config/', ''))) { - navigate({ to: targetRoute }) - } + if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) { + return + } + + const targetRoute = STEP_ROUTE_MAP[tourState.stepIndex] + if (targetRoute && window.location.pathname !== targetRoute) { + navigate({ to: targetRoute }) } }, [tourState.stepIndex, tourState.activeTourId, tourState.isRunning, navigate]) - // 监听 Tour 步骤变化,当从弹窗内步骤回退到弹窗外步骤时,自动关闭弹窗 - // 模型弹窗步骤: 12-17 (index 12-17),弹窗外步骤: 10-11 (index 10-11) useEffect(() => { - if (tourState.activeTourId === MODEL_ASSIGNMENT_TOUR_ID && tourState.isRunning) { - const prevStep = prevTourStepRef.current - const currentStep = tourState.stepIndex - - // 如果从弹窗内步骤 (12-17) 回退到弹窗外步骤 (<=11),关闭弹窗 - if (prevStep >= 12 && prevStep <= 17 && currentStep < 12) { - onCloseEditDialog?.() - } - - prevTourStepRef.current = currentStep + if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) { + return } - }, [tourState.stepIndex, tourState.activeTourId, tourState.isRunning, onCloseEditDialog]) - // 处理 Tour 中需要用户点击才能继续的步骤 + const prevStep = prevTourStepRef.current + const currentStep = tourState.stepIndex + + if (currentStep <= 2) { + onOpenProvidersTab?.() + } + + if (prevStep >= 3 && prevStep <= 9 && currentStep < 3) { + onCloseProviderDialog?.() + } + + if (prevStep <= 2 && currentStep >= 3 && currentStep <= 9) { + onOpenProviderDialog?.() + } + + if (currentStep === 10 || currentStep === 11) { + onCloseProviderDialog?.() + onOpenModelsTab?.() + } + + if (prevStep >= 12 && prevStep <= 17 && currentStep < 12) { + onCloseEditDialog?.() + } + + if (prevStep <= 11 && currentStep >= 12 && currentStep <= 17) { + onOpenEditDialog?.() + } + + if (currentStep === 19) { + onOpenTasksTab?.() + } + + prevTourStepRef.current = currentStep + }, [ + tourState.stepIndex, + tourState.activeTourId, + tourState.isRunning, + onOpenEditDialog, + onCloseEditDialog, + onOpenProviderDialog, + onCloseProviderDialog, + onOpenProvidersTab, + onOpenModelsTab, + onOpenTasksTab, + ]) + useEffect(() => { if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) return - const handleTourClick = (e: MouseEvent) => { + const handleTourClick = (event: MouseEvent) => { const currentStep = tourState.stepIndex - // Step 3 (index 2): 点击添加提供商按钮 - if (currentStep === 2 && didClickTourTarget(e, '[data-tour="add-provider-button"]')) { + if (currentStep === 1 && didClickTourTarget(event, '[data-tour="providers-tab-trigger"]')) { + onOpenProvidersTab?.() + setTimeout(() => goToStep(2), 300) + } else if (currentStep === 2 && didClickTourTarget(event, '[data-tour="add-provider-button"]')) { + onOpenProviderDialog?.() setTimeout(() => goToStep(3), 300) - } - // Step 10 (index 9): 点击取消按钮(关闭提供商弹窗) - else if (currentStep === 9 && didClickTourTarget(e, '[data-tour="provider-cancel-button"]')) { + } else if (currentStep === 9 && didClickTourTarget(event, '[data-tour="provider-cancel-button"]')) { + onCloseProviderDialog?.() setTimeout(() => goToStep(10), 300) - } - // Step 12 (index 11): 点击添加模型按钮 - else if (currentStep === 11 && didClickTourTarget(e, '[data-tour="add-model-button"]')) { + } else if (currentStep === 10 && didClickTourTarget(event, '[data-tour="models-tab-trigger"]')) { + onOpenModelsTab?.() + setTimeout(() => goToStep(11), 300) + } else if (currentStep === 11 && didClickTourTarget(event, '[data-tour="add-model-button"]')) { onOpenEditDialog?.() setTimeout(() => goToStep(12), 300) - } - // Step 18 (index 17): 点击取消按钮(关闭模型弹窗) - else if (currentStep === 17 && didClickTourTarget(e, '[data-tour="model-cancel-button"]')) { + } else if (currentStep === 17 && didClickTourTarget(event, '[data-tour="model-cancel-button"]')) { + onCloseEditDialog?.() setTimeout(() => goToStep(18), 300) - } - // Step 19 (index 18): 点击为模型分配功能标签页 - else if (currentStep === 18 && didClickTourTarget(e, '[data-tour="tasks-tab-trigger"]')) { + } else if (currentStep === 18 && didClickTourTarget(event, '[data-tour="tasks-tab-trigger"]')) { + onOpenTasksTab?.() setTimeout(() => goToStep(19), 300) } } document.addEventListener('click', handleTourClick, true) return () => document.removeEventListener('click', handleTourClick, true) - }, [tourState, goToStep, onOpenEditDialog, didClickTourTarget]) + }, [ + tourState, + goToStep, + onOpenEditDialog, + onCloseEditDialog, + onOpenProviderDialog, + onCloseProviderDialog, + onOpenProvidersTab, + onOpenModelsTab, + onOpenTasksTab, + didClickTourTarget, + ]) - // Step 12 的 spotlight 点击在部分浏览器/布局下会被 Joyride 遮罩截获。 - // 这里直接给目标按钮补一个原生监听,确保点中按钮时能打开模型弹窗。 - useEffect(() => { - if ( - tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || - !tourState.isRunning || - tourState.stepIndex !== 11 - ) { - return - } - - const addModelButton = document.querySelector('[data-tour="add-model-button"]') - if (!addModelButton) { - return - } - - const handleAddModelButtonClick = () => { - onOpenEditDialog?.() - setTimeout(() => goToStep(12), 300) - } - - addModelButton.addEventListener('click', handleAddModelButtonClick, true) - return () => addModelButton.removeEventListener('click', handleAddModelButtonClick, true) - }, [tourState.activeTourId, tourState.isRunning, tourState.stepIndex, goToStep, onOpenEditDialog]) - - // 开始引导 const handleStartTour = useCallback(() => { + onOpenProvidersTab?.() startTourFn(MODEL_ASSIGNMENT_TOUR_ID) - }, [startTourFn]) + }, [startTourFn, onOpenProvidersTab]) return { startTour: handleStartTour, diff --git a/src/A_memorix/core/runtime/sdk_memory_kernel.py b/src/A_memorix/core/runtime/sdk_memory_kernel.py index a9ca167c..4340aac0 100644 --- a/src/A_memorix/core/runtime/sdk_memory_kernel.py +++ b/src/A_memorix/core/runtime/sdk_memory_kernel.py @@ -3020,80 +3020,80 @@ class SDKMemoryKernel: @staticmethod def _feedback_cfg_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_enabled", False)) @staticmethod def _feedback_cfg_window_hours() -> float: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return max(0.1, float(getattr(memory_cfg, "feedback_correction_window_hours", 12.0) or 12.0)) @staticmethod def _feedback_cfg_check_interval_seconds() -> float: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration minutes = max(1, int(getattr(memory_cfg, "feedback_correction_check_interval_minutes", 30) or 30)) return float(minutes) * 60.0 @staticmethod def _feedback_cfg_batch_size() -> int: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return max(1, int(getattr(memory_cfg, "feedback_correction_batch_size", 20) or 20)) @staticmethod def _feedback_cfg_auto_apply_threshold() -> float: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration value = float(getattr(memory_cfg, "feedback_correction_auto_apply_threshold", 0.85) or 0.85) return min(1.0, max(0.0, value)) @staticmethod def _feedback_cfg_max_messages() -> int: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return max(1, int(getattr(memory_cfg, "feedback_correction_max_feedback_messages", 30) or 30)) @staticmethod def _feedback_cfg_prefilter_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_prefilter_enabled", True)) @staticmethod def _feedback_cfg_paragraph_mark_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_paragraph_mark_enabled", True)) @staticmethod def _feedback_cfg_paragraph_hard_filter_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_paragraph_hard_filter_enabled", True)) @staticmethod def _feedback_cfg_profile_refresh_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_profile_refresh_enabled", True)) @staticmethod def _feedback_cfg_profile_force_refresh_on_read() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_profile_force_refresh_on_read", True)) @staticmethod def _feedback_cfg_episode_rebuild_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_episode_rebuild_enabled", True)) @staticmethod def _feedback_cfg_episode_query_block_enabled() -> bool: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return bool(getattr(memory_cfg, "feedback_correction_episode_query_block_enabled", True)) @staticmethod def _feedback_cfg_reconcile_interval_seconds() -> float: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration minutes = max(1, int(getattr(memory_cfg, "feedback_correction_reconcile_interval_minutes", 5) or 5)) return float(minutes) * 60.0 @staticmethod def _feedback_cfg_reconcile_batch_size() -> int: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration return max(1, int(getattr(memory_cfg, "feedback_correction_reconcile_batch_size", 20) or 20)) @classmethod diff --git a/src/A_memorix/core/utils/episode_service.py b/src/A_memorix/core/utils/episode_service.py index 9c66f9c0..bf62b767 100644 --- a/src/A_memorix/core/utils/episode_service.py +++ b/src/A_memorix/core/utils/episode_service.py @@ -529,7 +529,7 @@ class EpisodeService: "paragraph_count": 0, } - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration paragraphs = self.metadata_store.get_live_paragraphs_by_source( token, exclude_stale=bool(getattr(memory_cfg, "feedback_correction_paragraph_hard_filter_enabled", True)), diff --git a/src/A_memorix/core/utils/person_profile_service.py b/src/A_memorix/core/utils/person_profile_service.py index f0a7f8c2..6215778b 100644 --- a/src/A_memorix/core/utils/person_profile_service.py +++ b/src/A_memorix/core/utils/person_profile_service.py @@ -349,7 +349,7 @@ class PersonProfileService: self, evidence: List[Dict[str, Any]], ) -> List[Dict[str, Any]]: - memory_cfg = getattr(global_config, "memory", None) + memory_cfg = global_config.a_memorix.integration if not bool(getattr(memory_cfg, "feedback_correction_paragraph_hard_filter_enabled", True)): return evidence paragraph_hashes = [ diff --git a/src/config/config.py b/src/config/config.py index 37d26f2b..50e40cff 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -27,7 +27,6 @@ from .official_configs import ( LogConfig, MaimMessageConfig, MCPConfig, - MemoryConfig, MessageReceiveConfig, PersonalityConfig, PluginRuntimeConfig, @@ -57,7 +56,7 @@ MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute( LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute() A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute() MMC_VERSION: str = "1.0.0-pre.11" -CONFIG_VERSION: str = "8.10.8" +CONFIG_VERSION: str = "8.10.9" MODEL_CONFIG_VERSION: str = "1.15.3" logger = get_logger("config") @@ -84,9 +83,6 @@ class Config(ConfigBase): expression: ExpressionConfig = Field(default_factory=ExpressionConfig) """表达配置类""" - memory: MemoryConfig = Field(default_factory=MemoryConfig) - """记忆配置类""" - a_memorix: AMemorixConfig = Field(default_factory=AMemorixConfig) """A_Memorix 长期记忆子系统配置""" diff --git a/src/config/legacy_migration.py b/src/config/legacy_migration.py index b5b4d0a3..82528933 100644 --- a/src/config/legacy_migration.py +++ b/src/config/legacy_migration.py @@ -392,6 +392,23 @@ def try_migrate_legacy_bot_config_dict(data: dict[str, Any]) -> MigrationResult: migrated_any = True reasons.append("visual.visual_style_removed") + memory = _as_dict(data.pop("memory", None)) + if memory is not None: + a_memorix = _as_dict(data.get("a_memorix")) + if a_memorix is None: + a_memorix = {} + data["a_memorix"] = a_memorix + + integration = _as_dict(a_memorix.get("integration")) + if integration is None: + integration = {} + a_memorix["integration"] = integration + + for key, value in memory.items(): + integration.setdefault(key, value) + migrated_any = True + reasons.append("memory->a_memorix.integration") + keyword_reaction = _as_dict(data.get("keyword_reaction")) if keyword_reaction is not None: if _drop_empty_keyword_rules(keyword_reaction, "keyword_rules"): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 6b249382..571e4cc4 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -467,8 +467,8 @@ class TargetItem(ConfigBase): """聊天流类型,group(群聊)或private(私聊)""" -class MemoryConfig(ConfigBase): - """记忆配置类""" +class AMemorixIntegrationConfig(ConfigBase): + """A_Memorix 与 Maisaka 集成配置""" __ui_parent__ = "a_memorix" @@ -1038,6 +1038,9 @@ class AMemorixConfig(ConfigBase): __ui_label__ = "长期记忆" __ui_icon__ = "brain" + integration: AMemorixIntegrationConfig = Field(default_factory=AMemorixIntegrationConfig) + """Maisaka 集成配置""" + plugin: AMemorixPluginConfig = Field(default_factory=AMemorixPluginConfig) """子系统状态""" diff --git a/src/main.py b/src/main.py index 6c98bc80..1c519453 100644 --- a/src/main.py +++ b/src/main.py @@ -157,15 +157,6 @@ class MainSystem: logger.info(t("startup.schedule_cancelled")) raise - # async def forget_memory_task(self): - # """记忆遗忘任务""" - # while True: - # await asyncio.sleep(global_config.memory.forget_memory_interval) - # logger.info("[记忆遗忘] 开始遗忘记忆...") - # await self.hippocampus_manager.forget_memory(percentage=global_config.memory.memory_forget_percentage) # type: ignore - # logger.info("[记忆遗忘] 记忆遗忘完成") - - async def main() -> None: """主函数""" system = MainSystem() diff --git a/src/maisaka/builtin_tool/__init__.py b/src/maisaka/builtin_tool/__init__.py index 0329aa62..dadd22e9 100644 --- a/src/maisaka/builtin_tool/__init__.py +++ b/src/maisaka/builtin_tool/__init__.py @@ -66,7 +66,7 @@ class BuiltinToolEntry: def _get_query_memory_tool_spec() -> ToolSpec: """根据配置生成 query_memory 工具声明。""" - return get_query_memory_tool_spec(enabled=bool(global_config.memory.enable_memory_query_tool)) + return get_query_memory_tool_spec(enabled=bool(global_config.a_memorix.integration.enable_memory_query_tool)) BUILTIN_TOOL_ENTRIES: List[BuiltinToolEntry] = [ diff --git a/src/maisaka/builtin_tool/query_memory.py b/src/maisaka/builtin_tool/query_memory.py index 3bd4b587..1a845ac1 100644 --- a/src/maisaka/builtin_tool/query_memory.py +++ b/src/maisaka/builtin_tool/query_memory.py @@ -161,7 +161,7 @@ async def handle_tool( f"不支持的检索模式:{mode}。可选值:search/time/hybrid/episode/aggregate。", ) - default_limit = max(1, global_config.memory.memory_query_default_limit) + default_limit = max(1, global_config.a_memorix.integration.memory_query_default_limit) try: limit = int(invocation.arguments.get("limit", default_limit) or default_limit) except (TypeError, ValueError): diff --git a/src/services/memory_flow_service.py b/src/services/memory_flow_service.py index 2dda0005..5ef7feb2 100644 --- a/src/services/memory_flow_service.py +++ b/src/services/memory_flow_service.py @@ -51,7 +51,7 @@ class PersonFactWritebackService: logger.warning("关闭人物事实写回 worker 失败: %s", exc) async def enqueue(self, message: Any) -> None: - if not bool(getattr(global_config.memory, "person_fact_writeback_enabled", True)): + if not bool(global_config.a_memorix.integration.person_fact_writeback_enabled): return if self._stopping: return @@ -251,7 +251,7 @@ class ChatSummaryWritebackService: logger.warning("关闭聊天摘要写回 worker 失败: %s", exc) async def enqueue(self, message: Any) -> None: - if not bool(getattr(global_config.memory, "chat_summary_writeback_enabled", True)): + if not bool(global_config.a_memorix.integration.chat_summary_writeback_enabled): return if self._stopping: return @@ -434,11 +434,11 @@ class ChatSummaryWritebackService: @staticmethod def _message_threshold() -> int: - return max(1, int(getattr(global_config.memory, "chat_summary_writeback_message_threshold", 12) or 12)) + return max(1, int(global_config.a_memorix.integration.chat_summary_writeback_message_threshold)) @staticmethod def _context_length() -> int: - return max(1, int(getattr(global_config.memory, "chat_summary_writeback_context_length", 50) or 50)) + return max(1, int(global_config.a_memorix.integration.chat_summary_writeback_context_length)) class MemoryAutomationService: diff --git a/src/webui/routers/config.py b/src/webui/routers/config.py index 1205f56d..f05e9e74 100644 --- a/src/webui/routers/config.py +++ b/src/webui/routers/config.py @@ -32,7 +32,6 @@ from src.config.official_configs import ( ExpressionConfig, KeywordReactionConfig, MaimMessageConfig, - MemoryConfig, MessageReceiveConfig, PersonalityConfig, ResponsePostProcessConfig, @@ -333,7 +332,6 @@ async def get_config_section_schema(section_name: str): - response_splitter: ResponseSplitterConfig - telemetry: TelemetryConfig - maim_message: MaimMessageConfig - - memory: MemoryConfig - debug: DebugConfig - voice: VoiceConfig - jargon: JargonConfig @@ -354,7 +352,6 @@ async def get_config_section_schema(section_name: str): "response_splitter": ResponseSplitterConfig, "telemetry": TelemetryConfig, "maim_message": MaimMessageConfig, - "memory": MemoryConfig, "a_memorix": AMemorixConfig, "debug": DebugConfig, "voice": VoiceConfig,