fear:持续获取maisak监控(壳开关

This commit is contained in:
SengokuCola
2026-05-04 18:05:52 +08:00
parent ccb1d60e06
commit f57fc1ff80
11 changed files with 254 additions and 502 deletions

View File

@@ -29,7 +29,6 @@ import { Code2, Info, Layout, Power, Save } from 'lucide-react'
import type { ConfigSchema } from '@/types/config-schema'
import {
ChatTalkValueRulesHook,
ExperimentalChatPromptsHook,
ExpressionGroupsHook,
ExpressionLearningListHook,
KeywordRulesHook,
@@ -53,9 +52,7 @@ const TAB_ORDER = [
'expression',
'emoji',
'response_post_process',
'lpmm_knowledge',
'webui',
'maisaka',
'plugin_runtime',
'log',
]
@@ -156,22 +153,18 @@ function BotConfigPageContent() {
const [expressionConfig, setExpressionConfig] = useState<ConfigSectionData | null>(null)
const [emojiConfig, setEmojiConfig] = useState<ConfigSectionData | null>(null)
const [memoryConfig, setMemoryConfig] = useState<ConfigSectionData | null>(null)
const [relationshipConfig, setRelationshipConfig] = useState<ConfigSectionData | null>(null)
const [visualConfig, setVisualConfig] = useState<ConfigSectionData | null>(null)
const [voiceConfig, setVoiceConfig] = useState<ConfigSectionData | null>(null)
const [messageReceiveConfig, setMessageReceiveConfig] = useState<ConfigSectionData | null>(null)
const [lpmmConfig, setLpmmConfig] = useState<ConfigSectionData | null>(null)
const [keywordReactionConfig, setKeywordReactionConfig] = useState<ConfigSectionData | null>(null)
const [responsePostProcessConfig, setResponsePostProcessConfig] = useState<ConfigSectionData | null>(null)
const [chineseTypoConfig, setChineseTypoConfig] = useState<ConfigSectionData | null>(null)
const [responseSplitterConfig, setResponseSplitterConfig] = useState<ConfigSectionData | null>(null)
const [debugConfig, setDebugConfig] = useState<ConfigSectionData | null>(null)
const [experimentalConfig, setExperimentalConfig] = useState<ConfigSectionData | null>(null)
const [maimMessageConfig, setMaimMessageConfig] = useState<ConfigSectionData | null>(null)
const [telemetryConfig, setTelemetryConfig] = useState<ConfigSectionData | null>(null)
const [webuiConfig, setWebuiConfig] = useState<ConfigSectionData | null>(null)
const [databaseConfig, setDatabaseConfig] = useState<ConfigSectionData | null>(null)
const [maisakaConfig, setMaisakaConfig] = useState<ConfigSectionData | null>(null)
const [mcpConfig, setMcpConfig] = useState<ConfigSectionData | null>(null)
const [pluginRuntimeConfig, setPluginRuntimeConfig] = useState<ConfigSectionData | null>(null)
const [aMemorixConfig, setAMemorixConfig] = useState<ConfigSectionData | null>(null)
@@ -255,22 +248,18 @@ function BotConfigPageContent() {
setExpressionConfig((config.expression ?? {}) as ConfigSectionData)
setEmojiConfig((config.emoji ?? {}) as ConfigSectionData)
setMemoryConfig((config.memory ?? {}) as ConfigSectionData)
setRelationshipConfig((config.relationship ?? {}) as ConfigSectionData)
setVisualConfig((config.visual ?? {}) as ConfigSectionData)
setVoiceConfig((config.voice ?? {}) as ConfigSectionData)
setMessageReceiveConfig((config.message_receive ?? {}) as ConfigSectionData)
setLpmmConfig((config.lpmm_knowledge ?? {}) as ConfigSectionData)
setKeywordReactionConfig((config.keyword_reaction ?? {}) as ConfigSectionData)
setResponsePostProcessConfig((config.response_post_process ?? {}) as ConfigSectionData)
setChineseTypoConfig((config.chinese_typo ?? {}) as ConfigSectionData)
setResponseSplitterConfig((config.response_splitter ?? {}) as ConfigSectionData)
setDebugConfig((config.debug ?? {}) as ConfigSectionData)
setExperimentalConfig((config.experimental ?? {}) as ConfigSectionData)
setMaimMessageConfig((config.maim_message ?? {}) as ConfigSectionData)
setTelemetryConfig((config.telemetry ?? {}) as ConfigSectionData)
setWebuiConfig((config.webui ?? {}) as ConfigSectionData)
setDatabaseConfig((config.database ?? {}) as ConfigSectionData)
setMaisakaConfig((config.maisaka ?? {}) as ConfigSectionData)
setMcpConfig((config.mcp ?? {}) as ConfigSectionData)
setPluginRuntimeConfig((config.plugin_runtime ?? {}) as ConfigSectionData)
setAMemorixConfig((config.a_memorix ?? {}) as ConfigSectionData)
@@ -289,22 +278,18 @@ function BotConfigPageContent() {
expression: expressionConfig,
emoji: emojiConfig,
memory: memoryConfig,
relationship: relationshipConfig,
visual: visualConfig,
voice: voiceConfig,
message_receive: messageReceiveConfig,
lpmm_knowledge: lpmmConfig,
keyword_reaction: keywordReactionConfig,
response_post_process: responsePostProcessConfig,
chinese_typo: chineseTypoConfig,
response_splitter: responseSplitterConfig,
debug: debugConfig,
experimental: experimentalConfig,
maim_message: maimMessageConfig,
telemetry: telemetryConfig,
webui: webuiConfig,
database: databaseConfig,
maisaka: maisakaConfig,
mcp: mcpConfig,
plugin_runtime: pluginRuntimeConfig,
a_memorix: aMemorixConfig,
@@ -316,22 +301,18 @@ function BotConfigPageContent() {
expressionConfig,
emojiConfig,
memoryConfig,
relationshipConfig,
visualConfig,
voiceConfig,
messageReceiveConfig,
lpmmConfig,
keywordReactionConfig,
responsePostProcessConfig,
chineseTypoConfig,
responseSplitterConfig,
debugConfig,
experimentalConfig,
maimMessageConfig,
telemetryConfig,
webuiConfig,
databaseConfig,
maisakaConfig,
mcpConfig,
pluginRuntimeConfig,
aMemorixConfig,
@@ -414,7 +395,6 @@ function BotConfigPageContent() {
useEffect(() => {
const hookEntries = [
['chat.talk_value_rules', ChatTalkValueRulesHook],
['experimental.chat_prompts', ExperimentalChatPromptsHook],
['expression.expression_groups', ExpressionGroupsHook],
['expression.learning_list', ExpressionLearningListHook],
['keyword_reaction.keyword_rules', KeywordRulesHook],
@@ -450,22 +430,18 @@ function BotConfigPageContent() {
useConfigAutoSave(expressionConfig, 'expression', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(emojiConfig, 'emoji', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(memoryConfig, 'memory', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(relationshipConfig, 'relationship', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(visualConfig, 'visual', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(voiceConfig, 'voice', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(messageReceiveConfig, 'message_receive', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(lpmmConfig, 'lpmm_knowledge', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(keywordReactionConfig, 'keyword_reaction', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(responsePostProcessConfig, 'response_post_process', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(chineseTypoConfig, 'chinese_typo', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(responseSplitterConfig, 'response_splitter', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(debugConfig, 'debug', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(experimentalConfig, 'experimental', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(maimMessageConfig, 'maim_message', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(telemetryConfig, 'telemetry', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(webuiConfig, 'webui', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(databaseConfig, 'database', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(maisakaConfig, 'maisaka', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(mcpConfig, 'mcp', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(pluginRuntimeConfig, 'plugin_runtime', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(aMemorixConfig, 'a_memorix', initialLoadRef.current, triggerAutoSave)
@@ -667,22 +643,18 @@ function BotConfigPageContent() {
expression: expressionConfig,
emoji: emojiConfig,
memory: memoryConfig,
relationship: relationshipConfig,
visual: visualConfig,
voice: voiceConfig,
message_receive: messageReceiveConfig,
lpmm_knowledge: lpmmConfig,
keyword_reaction: keywordReactionConfig,
response_post_process: responsePostProcessConfig,
chinese_typo: chineseTypoConfig,
response_splitter: responseSplitterConfig,
debug: debugConfig,
experimental: experimentalConfig,
maim_message: maimMessageConfig,
telemetry: telemetryConfig,
webui: webuiConfig,
database: databaseConfig,
maisaka: maisakaConfig,
mcp: mcpConfig,
plugin_runtime: pluginRuntimeConfig,
a_memorix: aMemorixConfig,
@@ -694,22 +666,18 @@ function BotConfigPageContent() {
expressionConfig,
emojiConfig,
memoryConfig,
relationshipConfig,
visualConfig,
voiceConfig,
messageReceiveConfig,
lpmmConfig,
keywordReactionConfig,
responsePostProcessConfig,
chineseTypoConfig,
responseSplitterConfig,
debugConfig,
experimentalConfig,
maimMessageConfig,
telemetryConfig,
webuiConfig,
databaseConfig,
maisakaConfig,
mcpConfig,
pluginRuntimeConfig,
aMemorixConfig,
@@ -724,22 +692,18 @@ function BotConfigPageContent() {
expression: setExpressionConfig,
emoji: setEmojiConfig,
memory: setMemoryConfig,
relationship: setRelationshipConfig,
visual: setVisualConfig,
voice: setVoiceConfig,
message_receive: setMessageReceiveConfig,
lpmm_knowledge: setLpmmConfig,
keyword_reaction: setKeywordReactionConfig,
response_post_process: setResponsePostProcessConfig,
chinese_typo: setChineseTypoConfig,
response_splitter: setResponseSplitterConfig,
debug: setDebugConfig,
experimental: setExperimentalConfig,
maim_message: setMaimMessageConfig,
telemetry: setTelemetryConfig,
webui: setWebuiConfig,
database: setDatabaseConfig,
maisaka: setMaisakaConfig,
mcp: setMcpConfig,
plugin_runtime: setPluginRuntimeConfig,
a_memorix: setAMemorixConfig,

View File

@@ -101,12 +101,6 @@ export const ExpressionGroupsHook = createJsonFieldHook({
placeholder: '[\n {\n "expression_groups": [\n {\n "platform": "qq",\n "item_id": "123456",\n "rule_type": "group"\n }\n ]\n }\n]',
})
export const ExperimentalChatPromptsHook = createJsonFieldHook({
emptyValue: [],
helperText: '实验配置中的定向 Prompt 列表使用 JSON 编辑。每一项应包含 platform、item_id、rule_type、prompt。',
placeholder: '[\n {\n "platform": "qq",\n "item_id": "123456",\n "rule_type": "group",\n "prompt": "这里填写额外提示词"\n }\n]',
})
export const MCPRootItemsHook = createJsonFieldHook({
emptyValue: [],
helperText: 'MCP Roots 条目为对象数组,使用 JSON 编辑。',

View File

@@ -12,7 +12,6 @@ export type {
} from './useAutoSave'
export {
ChatTalkValueRulesHook,
ExperimentalChatPromptsHook,
ExpressionGroupsHook,
ExpressionLearningListHook,
KeywordRulesHook,

View File

@@ -1,311 +0,0 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Plus, Trash2, AlertTriangle, Eye, Code2 } from 'lucide-react'
import type { ExperimentalConfig } from '../types'
interface ChatPromptData {
platform: string
id: string
type: 'group' | 'private'
prompt: string
}
interface ExperimentalSectionProps {
config: ExperimentalConfig
onChange: (config: ExperimentalConfig) => void
}
export const ExperimentalSection = React.memo(function ExperimentalSection({ config, onChange }: ExperimentalSectionProps) {
// 解析 chat_prompt 字符串为结构化数据
const parseChatPrompt = (promptStr: string): ChatPromptData => {
const parts = promptStr.split(':')
if (parts.length >= 4) {
const platform = parts[0]
const id = parts[1]
const type = parts[2] as 'group' | 'private'
const prompt = parts.slice(3).join(':') // 处理 prompt 中可能包含的冒号
return { platform, id, type, prompt }
}
return { platform: 'qq', id: '', type: 'group', prompt: '' }
}
// 将结构化数据转换为字符串
const stringifyChatPrompt = (data: ChatPromptData): string => {
return `${data.platform}:${data.id}:${data.type}:${data.prompt}`
}
const addChatPrompt = () => {
onChange({ ...config, chat_prompts: [...config.chat_prompts, 'qq::group:'] })
}
const removeChatPrompt = (index: number) => {
onChange({
...config,
chat_prompts: config.chat_prompts.filter((_, i) => i !== index),
})
}
const updateChatPrompt = (index: number, data: Partial<ChatPromptData>) => {
const currentData = parseChatPrompt(config.chat_prompts[index])
const newData = { ...currentData, ...data }
const newPrompts = [...config.chat_prompts]
newPrompts[index] = stringifyChatPrompt(newData)
onChange({ ...config, chat_prompts: newPrompts })
}
// 预览组件
const ChatPromptPreview = ({ promptStr }: { promptStr: string }) => {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 sm:w-96">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<div className="rounded-md bg-muted p-3 font-mono text-xs break-all">
"{promptStr}"
</div>
<p className="text-xs text-muted-foreground">
bot_config.toml
</p>
</div>
</PopoverContent>
</Popover>
)
}
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<div className="flex items-start gap-3 p-3 rounded-lg bg-orange-500/10 border border-orange-500/20">
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="font-medium text-orange-500"></h4>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid gap-6">
<div className="flex items-center space-x-2">
<Switch
id="lpmm_memory"
checked={config.lpmm_memory ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, lpmm_memory: checked })
}
/>
<Label htmlFor="lpmm_memory" className="cursor-pointer">
LPMM
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-4">
chat_history_summarizer
</p>
<div className="grid gap-2">
<Label htmlFor="private_plan_style"></Label>
<Textarea
id="private_plan_style"
value={config.private_plan_style}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, private_plan_style: e.target.value })}
placeholder="私聊的说话规则和行为风格(不推荐修改)"
rows={4}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="grid gap-4">
<div className="flex items-center justify-between">
<div>
<Label> Prompt </Label>
<p className="text-xs text-muted-foreground mt-1">
prompt
</p>
</div>
<Button onClick={addChatPrompt} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-4">
{config.chat_prompts.map((promptStr, index) => {
const data = parseChatPrompt(promptStr)
return (
<div key={index} className="rounded-lg border p-4 space-y-4 bg-card">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
Prompt {index + 1}
</span>
<div className="flex items-center gap-2">
<ChatPromptPreview promptStr={promptStr} />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="ghost">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
prompt
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeChatPrompt(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="grid gap-4">
{/* 平台选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={data.platform}
onValueChange={(value) => updateChatPrompt(index, { platform: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择平台" />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
<SelectItem value="webui">WebUI</SelectItem>
</SelectContent>
</Select>
</div>
{/* ID 输入 */}
<div className="grid gap-2">
<Label className="text-xs font-medium">
{data.type === 'group' ? '群号' : '用户ID'}
</Label>
<Input
value={data.id}
onChange={(e) => updateChatPrompt(index, { id: e.target.value })}
placeholder={data.type === 'group' ? '输入群号' : '输入用户ID'}
className="font-mono"
/>
</div>
{/* 类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={data.type}
onValueChange={(value: 'group' | 'private') => updateChatPrompt(index, { type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="group"> (group)</SelectItem>
<SelectItem value="private"> (private)</SelectItem>
</SelectContent>
</Select>
</div>
{/* Prompt 内容 */}
<div className="grid gap-2">
<Label className="text-xs font-medium">Prompt </Label>
<Textarea
value={data.prompt}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateChatPrompt(index, { prompt: e.target.value })}
placeholder="输入额外的 prompt 内容,例如:这是一个摄影群,你精通摄影知识"
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 原始格式显示 */}
<div className="rounded-md bg-muted/50 p-3">
<div className="flex items-center gap-2 mb-2">
<Code2 className="h-3 w-3 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground"></span>
</div>
<code className="text-xs font-mono text-muted-foreground break-all">
{promptStr || '(未配置)'}
</code>
</div>
</div>
</div>
)
})}
{config.chat_prompts.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm"> prompt </p>
<p className="text-xs mt-1">"添加配置"</p>
</div>
)}
</div>
{/* 使用说明 */}
<div className="text-xs text-muted-foreground space-y-2 p-4 rounded-lg bg-muted/30 border">
<p className="font-medium text-foreground">💡 使</p>
<ul className="list-disc list-inside space-y-1 pl-2">
<li></li>
<li>QQWebUI</li>
<li></li>
<li>Prompt </li>
</ul>
<p className="font-medium text-foreground mt-3">📝 </p>
<ul className="list-disc list-inside space-y-1 pl-2">
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
</ul>
</div>
</div>
</div>
</div>
</div>
)
})

View File

@@ -8,7 +8,6 @@ export { DreamSection } from './DreamSection'
export { LPMMSection } from './LPMMSection'
export { LogSection } from './LogSection'
export { DebugSection } from './DebugSection'
export { ExperimentalSection } from './ExperimentalSection'
export { MaimMessageSection } from './MaimMessageSection'
export { TelemetrySection } from './TelemetrySection'
export { FeaturesSection } from './FeaturesSection'

View File

@@ -189,12 +189,6 @@ export interface DebugConfig {
show_lpmm_paragraph: boolean
}
export interface ExperimentalConfig {
private_plan_style: string
chat_prompts: string[]
lpmm_memory: boolean
}
export interface MaimMessageConfig {
auth_token: string[]
enable_api_server: boolean
@@ -239,14 +233,12 @@ export interface AllBotConfigs {
voiceConfig: VoiceConfig | null
messageReceiveConfig: MessageReceiveConfig | null
dreamConfig: DreamConfig | null
lpmmConfig: LPMMKnowledgeConfig | null
keywordReactionConfig: KeywordReactionConfig | null
responsePostProcessConfig: ResponsePostProcessConfig | null
chineseTypoConfig: ChineseTypoConfig | null
responseSplitterConfig: ResponseSplitterConfig | null
logConfig: LogConfig | null
debugConfig: DebugConfig | null
experimentalConfig: ExperimentalConfig | null
maimMessageConfig: MaimMessageConfig | null
telemetryConfig: TelemetryConfig | null
}
@@ -261,25 +253,21 @@ export type ConfigSectionName =
| 'expression'
| 'emoji'
| 'memory'
| 'relationship'
| 'visual'
| 'tool'
| 'voice'
| 'message_receive'
| 'dream'
| 'lpmm_knowledge'
| 'keyword_reaction'
| 'response_post_process'
| 'chinese_typo'
| 'response_splitter'
| 'log'
| 'debug'
| 'experimental'
| 'maim_message'
| 'telemetry'
| 'webui'
| 'database'
| 'maisaka'
| 'mcp'
| 'plugin_runtime'
| 'a_memorix'

View File

@@ -19,6 +19,7 @@ import {
Gauge,
MessageSquare,
PauseCircle,
Radio,
Timer,
Wrench,
XCircle,
@@ -585,6 +586,8 @@ export function MaisakaMonitor() {
selectedSession,
setSelectedSession,
connected,
backgroundCollection,
setBackgroundCollectionEnabled,
clearTimeline,
} = useMaisakaMonitor()
@@ -697,6 +700,16 @@ export function MaisakaMonitor() {
</div>
</div>
<div className="ml-auto flex items-center gap-2">
<Button
variant={backgroundCollection ? 'secondary' : 'ghost'}
size="sm"
className="h-7 text-xs"
onClick={() => setBackgroundCollectionEnabled(!backgroundCollection)}
title={backgroundCollection ? '关闭离开页面后的持续获取' : '开启离开页面后的持续获取'}
>
<Radio className={cn('h-3.5 w-3.5 mr-1', backgroundCollection && 'text-primary')} />
</Button>
<Button
variant={showCycleMarkers ? 'secondary' : 'ghost'}
size="sm"
@@ -746,17 +759,19 @@ export function MaisakaMonitor() {
</div>
) : (
(() => {
const displayedTimingGateCycles = new Set<string>()
const continuedTimingGateCycles = new Set<string>()
return timeline.map((entry) => {
if (entry.type === 'timing_gate.result') {
const data = entry.data as TimingGateResultEvent
displayedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id))
if (data.action === 'continue') {
continuedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id))
}
}
if (entry.type === 'planner.response' || entry.type === 'planner.finalized') {
const data = entry.data as PlannerResponseEvent | PlannerFinalizedEvent
if (!displayedTimingGateCycles.has(buildCycleKey(data.session_id, data.cycle_id))) {
if (!continuedTimingGateCycles.has(buildCycleKey(data.session_id, data.cycle_id))) {
return null
}
}

View File

@@ -3,7 +3,7 @@
*
* 管理 WebSocket 订阅与事件流的状态。
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import type { MaisakaMonitorEvent } from '@/lib/maisaka-monitor-client'
import { maisakaMonitorClient } from '@/lib/maisaka-monitor-client'
@@ -36,6 +36,7 @@ export interface SessionInfo {
/** 最大保留的时间线条目数 */
const MAX_TIMELINE_ENTRIES = 500
const BACKGROUND_COLLECTION_STORAGE_KEY = 'maisaka-monitor-background-collection'
function resolveSessionDisplayName({
fallbackName,
@@ -72,152 +73,219 @@ let entryCounter = 0
let cachedTimeline: TimelineEntry[] = []
let cachedSessions: Map<string, SessionInfo> = new Map()
let cachedSelectedSession: string | null = null
let cachedConnected = false
let backgroundCollectionEnabled = false
let backgroundCollectionPreferenceLoaded = false
let activeConsumerCount = 0
let monitorSubscriptionStarted = false
let monitorSubscriptionPromise: Promise<void> | null = null
let monitorUnsubscribe: (() => Promise<void>) | null = null
const storeListeners = new Set<() => void>()
function notifyStoreListeners() {
storeListeners.forEach((listener) => listener())
}
function loadBackgroundCollectionPreference() {
if (backgroundCollectionPreferenceLoaded) {
return backgroundCollectionEnabled
}
backgroundCollectionPreferenceLoaded = true
if (typeof window !== 'undefined') {
backgroundCollectionEnabled = window.localStorage.getItem(BACKGROUND_COLLECTION_STORAGE_KEY) === 'true'
}
return backgroundCollectionEnabled
}
function shouldKeepMonitorActive() {
return activeConsumerCount > 0 || backgroundCollectionEnabled
}
function appendTimelineEntry(entry: TimelineEntry) {
const next = [...cachedTimeline, entry]
cachedTimeline = next.length > MAX_TIMELINE_ENTRIES
? next.slice(next.length - MAX_TIMELINE_ENTRIES)
: next
}
function updateSessionInfo(event: MaisakaMonitorEvent, sessionId: string, timestamp: number) {
const dataRecord = event.data as unknown as Record<string, unknown>
const isGroupChat = typeof dataRecord.is_group_chat === 'boolean'
? dataRecord.is_group_chat
: undefined
const groupId = typeof dataRecord.group_id === 'string' ? dataRecord.group_id : null
const userId = typeof dataRecord.user_id === 'string' ? dataRecord.user_id : null
const platform = typeof dataRecord.platform === 'string' ? dataRecord.platform : undefined
const sessionName = typeof dataRecord.session_name === 'string'
? dataRecord.session_name
: undefined
const next = new Map(cachedSessions)
const existing = next.get(sessionId)
if (event.type === 'session.start' || !existing) {
next.set(sessionId, {
sessionId,
sessionName: resolveSessionDisplayName({
fallbackName: sessionName,
groupId,
isGroupChat,
sessionId,
userId,
}),
isGroupChat,
groupId,
userId,
platform,
lastActivity: timestamp,
eventCount: (existing?.eventCount ?? 0) + 1,
})
} else {
next.set(sessionId, {
...existing,
sessionName: resolveSessionDisplayName({
fallbackName: sessionName ?? existing.sessionName,
groupId: groupId ?? existing.groupId,
isGroupChat: isGroupChat ?? existing.isGroupChat,
sessionId,
userId: userId ?? existing.userId,
}),
isGroupChat: isGroupChat ?? existing.isGroupChat,
groupId: groupId ?? existing.groupId,
userId: userId ?? existing.userId,
platform: platform ?? existing.platform,
lastActivity: timestamp,
eventCount: existing.eventCount + 1,
})
}
cachedSessions = next
}
function handleMonitorEvent(event: MaisakaMonitorEvent) {
const dataRecord = event.data as unknown as Record<string, unknown>
const sessionId = dataRecord.session_id as string
const timestamp = dataRecord.timestamp as number
if (!sessionId || typeof timestamp !== 'number') {
return
}
appendTimelineEntry({
id: `evt_${++entryCounter}_${Date.now()}`,
type: event.type,
data: event.data,
timestamp,
sessionId,
})
updateSessionInfo(event, sessionId, timestamp)
if (cachedSelectedSession === null) {
cachedSelectedSession = sessionId
}
notifyStoreListeners()
}
function ensureMonitorSubscription() {
if (monitorSubscriptionStarted || monitorSubscriptionPromise !== null) {
return
}
monitorSubscriptionPromise = maisakaMonitorClient
.subscribe(handleMonitorEvent)
.then((unsub) => {
monitorUnsubscribe = unsub
if (!shouldKeepMonitorActive()) {
monitorUnsubscribe = null
void unsub()
cachedConnected = false
notifyStoreListeners()
return
}
monitorSubscriptionStarted = true
cachedConnected = true
notifyStoreListeners()
})
.catch((error) => {
console.error('MaiSaka 监控订阅失败:', error)
cachedConnected = false
notifyStoreListeners()
})
.finally(() => {
monitorSubscriptionPromise = null
})
}
function stopMonitorSubscriptionIfIdle() {
if (shouldKeepMonitorActive()) {
return
}
if (monitorUnsubscribe) {
const unsub = monitorUnsubscribe
monitorUnsubscribe = null
monitorSubscriptionStarted = false
cachedConnected = false
notifyStoreListeners()
void unsub()
}
}
export function useMaisakaMonitor() {
const [timeline, setTimeline] = useState<TimelineEntry[]>(cachedTimeline)
const [sessions, setSessions] = useState<Map<string, SessionInfo>>(new Map(cachedSessions))
const [selectedSession, setSelectedSessionState] = useState<string | null>(cachedSelectedSession)
const [connected, setConnected] = useState(false)
const unsubRef = useRef<(() => Promise<void>) | null>(null)
const handleEvent = useCallback((event: MaisakaMonitorEvent) => {
const dataRecord = event.data as unknown as Record<string, unknown>
const sessionId = dataRecord.session_id as string
const timestamp = dataRecord.timestamp as number
const isGroupChat = typeof dataRecord.is_group_chat === 'boolean'
? dataRecord.is_group_chat
: undefined
const groupId = typeof dataRecord.group_id === 'string' ? dataRecord.group_id : null
const userId = typeof dataRecord.user_id === 'string' ? dataRecord.user_id : null
const platform = typeof dataRecord.platform === 'string' ? dataRecord.platform : undefined
const sessionName = typeof dataRecord.session_name === 'string'
? dataRecord.session_name
: undefined
const entry: TimelineEntry = {
id: `evt_${++entryCounter}_${Date.now()}`,
type: event.type,
data: event.data,
timestamp,
sessionId,
}
setTimeline((prev) => {
const next = [...prev, entry]
const trimmed = next.length > MAX_TIMELINE_ENTRIES
? next.slice(next.length - MAX_TIMELINE_ENTRIES)
: next
cachedTimeline = trimmed
return trimmed
})
// 更新会话信息
if (event.type === 'session.start') {
setSessions((prev) => {
const next = new Map(prev)
next.set(sessionId, {
sessionId,
sessionName: resolveSessionDisplayName({
fallbackName: sessionName,
groupId,
isGroupChat,
sessionId,
userId,
}),
isGroupChat,
groupId,
userId,
platform,
lastActivity: timestamp,
eventCount: (prev.get(sessionId)?.eventCount ?? 0) + 1,
})
cachedSessions = next
return next
})
} else {
setSessions((prev) => {
const existing = prev.get(sessionId)
if (!existing) {
const next = new Map(prev)
next.set(sessionId, {
sessionId,
sessionName: resolveSessionDisplayName({
fallbackName: sessionName,
groupId,
isGroupChat,
sessionId,
userId,
}),
isGroupChat,
groupId,
userId,
platform,
lastActivity: timestamp,
eventCount: 1,
})
cachedSessions = next
return next
}
const next = new Map(prev)
next.set(sessionId, {
...existing,
sessionName: resolveSessionDisplayName({
fallbackName: sessionName ?? existing.sessionName,
groupId: groupId ?? existing.groupId,
isGroupChat: isGroupChat ?? existing.isGroupChat,
sessionId,
userId: userId ?? existing.userId,
}),
isGroupChat: isGroupChat ?? existing.isGroupChat,
groupId: groupId ?? existing.groupId,
userId: userId ?? existing.userId,
platform: platform ?? existing.platform,
lastActivity: timestamp,
eventCount: existing.eventCount + 1,
})
cachedSessions = next
return next
})
}
// 自动选中第一个会话
setSelectedSessionState((current) => {
const next = current ?? sessionId
cachedSelectedSession = next
return next
})
}, [])
const [connected, setConnected] = useState(cachedConnected)
const [backgroundCollection, setBackgroundCollection] = useState(loadBackgroundCollectionPreference)
useEffect(() => {
let cancelled = false
maisakaMonitorClient.subscribe(handleEvent).then((unsub) => {
if (cancelled) {
void unsub()
return
}
unsubRef.current = unsub
setConnected(true)
})
return () => {
cancelled = true
if (unsubRef.current) {
void unsubRef.current()
unsubRef.current = null
}
setConnected(false)
activeConsumerCount += 1
ensureMonitorSubscription()
const syncFromStore = () => {
setTimeline(cachedTimeline)
setSessions(new Map(cachedSessions))
setSelectedSessionState(cachedSelectedSession)
setConnected(cachedConnected)
setBackgroundCollection(backgroundCollectionEnabled)
}
}, [handleEvent])
storeListeners.add(syncFromStore)
syncFromStore()
return () => {
storeListeners.delete(syncFromStore)
activeConsumerCount = Math.max(0, activeConsumerCount - 1)
stopMonitorSubscriptionIfIdle()
}
}, [])
const clearTimeline = useCallback(() => {
cachedTimeline = []
setTimeline([])
notifyStoreListeners()
}, [])
const setSelectedSession = useCallback((sessionId: string | null) => {
cachedSelectedSession = sessionId
setSelectedSessionState(sessionId)
notifyStoreListeners()
}, [])
const setBackgroundCollectionEnabled = useCallback((enabled: boolean) => {
backgroundCollectionEnabled = enabled
backgroundCollectionPreferenceLoaded = true
if (typeof window !== 'undefined') {
window.localStorage.setItem(BACKGROUND_COLLECTION_STORAGE_KEY, String(enabled))
}
if (enabled) {
ensureMonitorSubscription()
} else {
stopMonitorSubscriptionIfIdle()
}
notifyStoreListeners()
}, [])
/** 当前选中会话的时间线 */
@@ -232,6 +300,8 @@ export function useMaisakaMonitor() {
selectedSession,
setSelectedSession,
connected,
backgroundCollection,
setBackgroundCollectionEnabled,
clearTimeline,
}
}

View File

@@ -17,9 +17,44 @@ from src.maisaka.builtin_tool import reply as reply_tool_module
from src.maisaka.builtin_tool import send_emoji as send_emoji_tool_module
from src.maisaka.monitor_events import emit_planner_finalized
from src.maisaka.reasoning_engine import MaisakaReasoningEngine
from src.maisaka import runtime as runtime_module
from src.maisaka.runtime import MaisakaHeartFlowChatting
def test_runtime_maps_expression_config_flags_to_correct_fields(monkeypatch: pytest.MonkeyPatch) -> None:
fake_chat_stream = SimpleNamespace(
is_group_session=True,
group_id="group-1",
user_id="user-1",
platform="test",
)
monkeypatch.setattr(
runtime_module.chat_manager,
"get_session_by_session_id",
lambda session_id: fake_chat_stream,
)
monkeypatch.setattr(runtime_module.chat_manager, "get_session_name", lambda session_id: "测试会话")
monkeypatch.setattr(
runtime_module.ExpressionConfigUtils,
"get_expression_config_for_chat",
staticmethod(lambda session_id: (True, False, True)),
)
monkeypatch.setattr(runtime_module, "ExpressionLearner", lambda session_id: SimpleNamespace())
monkeypatch.setattr(runtime_module, "JargonMiner", lambda session_id, session_name: SimpleNamespace())
monkeypatch.setattr(runtime_module, "MaisakaReasoningEngine", lambda runtime: SimpleNamespace())
monkeypatch.setattr(runtime_module, "ToolRegistry", lambda: SimpleNamespace())
monkeypatch.setattr(runtime_module, "ReplyEffectTracker", lambda **kwargs: SimpleNamespace())
monkeypatch.setattr(MaisakaHeartFlowChatting, "_register_tool_providers", lambda self: None)
monkeypatch.setattr(MaisakaHeartFlowChatting, "_emit_monitor_session_start", lambda self: None)
runtime = MaisakaHeartFlowChatting("session-1")
assert runtime._enable_expression_use is True
assert runtime._enable_expression_learning is False
assert runtime._enable_jargon_learning is True
class _FakeLLMResult:
def __init__(self) -> None:
self.response = "测试回复"

View File

@@ -9,7 +9,6 @@ httpx
jieba>=0.42.1
json-repair>=0.47.6
maim-message>=0.6.2
maibot-dashboard>=1.0.2.dev2026050359
maibot-plugin-sdk>=2.4.0
matplotlib>=3.10.5
mcp

View File

@@ -127,7 +127,7 @@ class MaisakaHeartFlowChatting:
int(global_config.chat.planner_interrupt_max_consecutive_count),
)
expr_use, jargon_learn, expr_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id)
expr_use, expr_learn, jargon_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id)
self._enable_expression_use = expr_use
self._enable_expression_learning = expr_learn
self._enable_jargon_learning = jargon_learn