diff --git a/.gitignore b/.gitignore index 4fd1a457..a9cceec5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ nonebot-maibot-adapter/ MaiMBot-LPMM *.zip run_bot.bat -run_na.bat run_all_in_wt.bat run.bat log_debug/ @@ -37,10 +36,6 @@ run_amds.bat run_none.bat docs-mai/ run.py -message_queue_content.txt -message_queue_content.bat -message_queue_window.bat -message_queue_window.txt queue_update.txt start_saka.bat .env @@ -50,8 +45,6 @@ start_all.bat config/bot_config_dev.toml config/bot_config.toml config/bot_config.toml.bak -config/lpmm_config.toml -config/lpmm_config.toml.bak template/compare/bot_config_template.toml template/compare/model_config_template.toml # CLAUDE.md diff --git a/dashboard/package.json b/dashboard/package.json index 85e72315..234c422a 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,7 +1,7 @@ { "name": "maibot-dashboard", "private": true, - "version": "1.0.5", + "version": "1.0.6", "type": "module", "main": "./out/main/index.js", "scripts": { diff --git a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx index 4bc866ff..012c2dbd 100644 --- a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx +++ b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx @@ -24,6 +24,7 @@ export interface DynamicConfigFormProps { /** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */ level?: number advancedVisible?: boolean + sectionColumns?: 1 | 2 } function buildFieldPath(basePath: string, fieldName: string) { @@ -126,6 +127,7 @@ function DynamicConfigSection({ hooks={hooks} level={level} advancedVisible={hasAdvanced ? advancedVisible : undefined} + sectionColumns={1} /> @@ -150,6 +152,7 @@ export const DynamicConfigForm: React.FC = ({ hooks = fieldHooks, level = 0, advancedVisible, + sectionColumns = 1, }) => { const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false) const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible @@ -161,10 +164,12 @@ export const DynamicConfigForm: React.FC = ({ const renderField = (field: FieldSchema) => { const fieldPath = buildFieldPath(basePath, field.name) + const nestedSchema = schema.nested?.[field.name] if (hooks.has(fieldPath)) { const hookEntry = hooks.get(fieldPath) if (!hookEntry) return null + if (hookEntry.type === 'hidden') return null const HookComponent = hookEntry.component @@ -174,7 +179,9 @@ export const DynamicConfigForm: React.FC = ({ fieldPath={fieldPath} value={values[field.name]} onChange={(v) => onChange(field.name, v)} + onParentChange={onChange} schema={field} + nestedSchema={nestedSchema} parentValues={values} /> ) @@ -185,7 +192,9 @@ export const DynamicConfigForm: React.FC = ({ fieldPath={fieldPath} value={values[field.name]} onChange={(v) => onChange(field.name, v)} + onParentChange={onChange} schema={field} + nestedSchema={nestedSchema} parentValues={values} > = ({ ) } - const topLevelFields = schema.fields.filter( - (field) => !schema.nested?.[field.name], + const shouldRenderFieldInline = (field: FieldSchema) => { + const fieldPath = buildFieldPath(basePath, field.name) + if (hooks.get(fieldPath)?.type === 'hidden') { + return false + } + + if (!schema.nested?.[field.name]) { + return true + } + + return hooks.get(fieldPath)?.type === 'replace' + } + + const inlineFields = schema.fields.filter(shouldRenderFieldInline) + const inlineNestedFieldNames = new Set( + inlineFields + .filter((field) => Boolean(schema.nested?.[field.name])) + .map((field) => field.name), ) - const normalFields = topLevelFields.filter((field) => !field.advanced) - const advancedFields = topLevelFields.filter((field) => field.advanced) + const normalFields = inlineFields.filter((field) => !field.advanced) + const advancedFields = inlineFields.filter((field) => field.advanced) const visibleFields = resolvedAdvancedVisible ? [...normalFields, ...advancedFields] : normalFields @@ -244,23 +269,32 @@ export const DynamicConfigForm: React.FC = ({ return rows } + const renderRows = (rows: FieldSchema[][]) => ( + <> + {rows.map((row) => ( + row.length > 1 ? ( +
field.name).join('|')} + className="grid gap-4 py-1 md:grid-cols-[repeat(var(--field-row-count),minmax(0,1fr))]" + style={{ '--field-row-count': row.length } as React.CSSProperties} + > + {row.map((field) => ( +
{renderField(field)}
+ ))} +
+ ) : ( +
{renderField(row[0])}
+ ) + ))} + + ) + const renderFieldList = (fields: FieldSchema[]) => ( <> {groupFieldsByRow(fields).map((row, index) => ( field.name).join('|')}> {index > 0 && } - {row.length > 1 ? ( -
- {row.map((field) => ( -
{renderField(field)}
- ))} -
- ) : ( -
{renderField(row[0])}
- )} + {renderRows([row])}
))} @@ -268,7 +302,7 @@ export const DynamicConfigForm: React.FC = ({ return (
- {topLevelFields.length > 0 && ( + {inlineFields.length > 0 && (
{advancedVisible === undefined && advancedFields.length > 0 && (
@@ -283,7 +317,9 @@ export const DynamicConfigForm: React.FC = ({ )} {schema.nested && - Object.entries(schema.nested) + (() => { + const nestedSections = Object.entries(schema.nested) + .filter(([key]) => !inlineNestedFieldNames.has(key)) .map(([key, nestedSchema]) => { const nestedField = fieldMap.get(key) const nestedFieldPath = buildFieldPath(basePath, key) @@ -299,8 +335,9 @@ export const DynamicConfigForm: React.FC = ({ onChange(key, v)} - schema={nestedField ?? nestedSchema} + onChange={(v) => onChange(key, v)} + onParentChange={onChange} + schema={nestedField ?? nestedSchema} nestedSchema={nestedSchema} parentValues={values} /> @@ -314,6 +351,7 @@ export const DynamicConfigForm: React.FC = ({ fieldPath={nestedFieldPath} value={values[key]} onChange={(v) => onChange(key, v)} + onParentChange={onChange} schema={nestedField ?? nestedSchema} nestedSchema={nestedSchema} parentValues={values} @@ -325,6 +363,7 @@ export const DynamicConfigForm: React.FC = ({ basePath={nestedFieldPath} hooks={hooks} level={level + 1} + sectionColumns={1} />
@@ -376,11 +415,27 @@ export const DynamicConfigForm: React.FC = ({ basePath={nestedFieldPath} hooks={hooks} level={level + 1} + sectionColumns={1} /> ) - })} + }) + + const visibleNestedSections = nestedSections.filter( + (section): section is React.ReactElement => Boolean(section), + ) + + if (level === 0 && sectionColumns === 2 && visibleNestedSections.length > 1) { + return ( +
+ {visibleNestedSections} +
+ ) + } + + return visibleNestedSections + })()}
) } diff --git a/dashboard/src/components/dynamic-form/DynamicField.tsx b/dashboard/src/components/dynamic-form/DynamicField.tsx index 27039cbe..2046717f 100644 --- a/dashboard/src/components/dynamic-form/DynamicField.tsx +++ b/dashboard/src/components/dynamic-form/DynamicField.tsx @@ -1,5 +1,6 @@ import * as React from "react" import * as LucideIcons from "lucide-react" +import { useTranslation } from "react-i18next" import { Input } from "@/components/ui/input" import { KeyValueEditor } from "@/components/ui/key-value-editor" @@ -15,6 +16,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" +import { resolveFieldLabel } from "@/lib/config-label" import type { FieldSchema } from "@/types/config-schema" export interface DynamicFieldProps { @@ -37,6 +39,8 @@ export const DynamicField: React.FC = ({ value, onChange, }) => { + const { i18n } = useTranslation() + const fieldLabel = resolveFieldLabel(schema, i18n.language) const isNumericField = schema.type === 'integer' || schema.type === 'number' const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => { @@ -126,17 +130,17 @@ export const DynamicField: React.FC = ({ const inlineDescription = hasOptionDescriptions ? '' : schema.description const renderFieldHeader = () => ( -
+
{inlineDescription && ( @@ -357,7 +361,7 @@ export const DynamicField: React.FC = ({ return ( onChange?.(event.target.value)} + /> +
+
+ + onParentChange?.('qq_account', event.target.value)} + /> +
+
+ + 主 + +
+
+ + {rows.map((row, rowIndex) => ( +
+
+ + updateRow(rowIndex, { platform: event.target.value })} + /> +
+
+ + updateRow(rowIndex, { account: event.target.value })} + /> +
+
+ +
+
+ ))} +
+ + ) +} + export const KeywordRulesHook = createListItemEditorHook({ addLabel: '添加关键词规则', helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。', diff --git a/dashboard/src/routes/config/bot/hooks/index.ts b/dashboard/src/routes/config/bot/hooks/index.ts index 3511fb0d..65c0791b 100644 --- a/dashboard/src/routes/config/bot/hooks/index.ts +++ b/dashboard/src/routes/config/bot/hooks/index.ts @@ -12,11 +12,13 @@ export type { } from './useAutoSave' export { BotPlatformsHook, + BotPlatformAccountsHook, ChatPromptsHook, ChatTalkValueRulesHook, ExpressionGroupsHook, ExpressionLearningListHook, KeywordRulesHook, + HiddenFieldHook, MCPRootItemsHook, MCPServersHook, RegexRulesHook, diff --git a/dashboard/src/routes/config/model.tsx b/dashboard/src/routes/config/model.tsx index a4d8c59c..01fd801d 100644 --- a/dashboard/src/routes/config/model.tsx +++ b/dashboard/src/routes/config/model.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef, type MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -47,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, Lock, Unlock } from 'lucide-react' +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 { resolveFieldLabel } from '@/lib/config-label' import type { ConfigSchema } from '@/types/config-schema' import { useToast } from '@/hooks/use-toast' import { Alert, AlertDescription } from '@/components/ui/alert' @@ -82,6 +84,7 @@ export function ModelConfigPage() { // 内部实现组件 function ModelConfigPageContent() { + const { i18n } = useTranslation() const [models, setModels] = useState([]) const [providers, setProviders] = useState([]) const [providerConfigs, setProviderConfigs] = useState([]) @@ -105,7 +108,6 @@ function ModelConfigPageContent() { const [pageSize, setPageSize] = useState(20) const [jumpToPage, setJumpToPage] = useState('') - const [advancedTemperatureMode, setAdvancedTemperatureMode] = useState(false) const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false) const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false) const [restartNoticeVisible, setRestartNoticeVisible] = useState( @@ -1009,15 +1011,11 @@ function ModelConfigPageContent() { {taskConfigSchema.fields .filter(f => f.type === 'object' && (advancedTaskSettingsVisible || !f.advanced)) .map((field, index) => { - const desc = field.description || field.name - const commaIdx = desc.search(/[,,]/) - const title = commaIdx > 0 ? desc.slice(0, commaIdx).trim() : desc - const subtitle = commaIdx > 0 ? desc.slice(commaIdx + 1).trim() : '' return ( updateTaskConfig(field.name, f, value)} @@ -1425,7 +1423,7 @@ function ModelConfigPageContent() { checked={editingModel?.temperature != null} onCheckedChange={(checked) => { if (checked) { - setEditingModel((prev) => prev ? { ...prev, temperature: 0.5 } : null) + setEditingModel((prev) => prev ? { ...prev, temperature: 0.7 } : null) } else { setEditingModel((prev) => prev ? { ...prev, temperature: null } : null) } @@ -1437,43 +1435,28 @@ function ModelConfigPageContent() {
-
- { - const value = parseFloat(e.target.value) - if (!isNaN(value) && value >= 0 && value <= 2) { - setEditingModel((prev) => prev ? { ...prev, temperature: value } : null) - } - }} - onBlur={(e) => { - const value = parseFloat(e.target.value) - if (isNaN(value) || value < 0) { - setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null) - } else if (value > 2) { - setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null) - } - }} - step={0.01} - min={0} - max={2} - className="w-20 h-8 text-sm text-right tabular-nums" - /> - -
+ { + const value = parseFloat(e.target.value) + if (!isNaN(value) && value >= 0 && value <= 2) { + setEditingModel((prev) => prev ? { ...prev, temperature: value } : null) + } + }} + onBlur={(e) => { + const value = parseFloat(e.target.value) + if (isNaN(value) || value < 0) { + setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null) + } else if (value > 2) { + setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null) + } + }} + step={0.01} + min={0} + max={2} + className="w-20 h-8 text-sm text-right tabular-nums" + />
0 @@ -1485,25 +1468,22 @@ function ModelConfigPageContent() { ) } min={0} - max={advancedTemperatureMode ? 2 : 1} - step={advancedTemperatureMode ? 0.05 : 0.1} + max={2} + step={0.05} className="flex-1" /> - {advancedTemperatureMode ? '2' : '1'} + 2
- {advancedTemperatureMode && ( + {editingModel.temperature > 1 && ( - 高级模式:温度 > 1 会产生更随机、更不可预测的输出,请谨慎使用 + 温度 > 1 会产生更随机、更不可预测的输出,请谨慎使用 )}

- {advancedTemperatureMode - ? "较低(0.1-0.5)产生确定输出,中等(0.5-1.0)平衡创造性,较高(1.0-2.0)产生极度随机输出" - : "较低的温度(0.1-0.3)产生更确定的输出,较高的温度(0.7-1.0)产生更多样化的输出" - } + 较低(0.1-0.5)产生确定输出,中等(0.5-1.0)平衡创造性,较高(1.0-2.0)产生极度随机输出

)} diff --git a/dashboard/src/routes/config/model/components/TaskConfigCard.tsx b/dashboard/src/routes/config/model/components/TaskConfigCard.tsx index 7e87a3c5..67234d38 100644 --- a/dashboard/src/routes/config/model/components/TaskConfigCard.tsx +++ b/dashboard/src/routes/config/model/components/TaskConfigCard.tsx @@ -104,11 +104,11 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({ type="number" step="0.1" min="0" - max="1" - value={taskConfig.temperature ?? 0.3} + max="2" + value={taskConfig.temperature ?? 0.7} onChange={(e) => { const value = parseFloat(e.target.value) - if (!isNaN(value) && value >= 0 && value <= 1) { + if (!isNaN(value) && value >= 0 && value <= 2) { onChange('temperature', value) } }} @@ -116,10 +116,10 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({ /> onChange('temperature', values[0])} min={0} - max={1} + max={2} step={0.1} className="w-full" /> diff --git a/dashboard/src/routes/config/modelProvider/index.tsx b/dashboard/src/routes/config/modelProvider/index.tsx index a14b7595..4933bdfc 100644 --- a/dashboard/src/routes/config/modelProvider/index.tsx +++ b/dashboard/src/routes/config/modelProvider/index.tsx @@ -742,7 +742,7 @@ function ModelProviderConfigPageContent() { {/* 页面标题 */}
-

AI模型厂商配置

+

模型厂商设置

管理 AI 模型厂商的 API 配置

diff --git a/dashboard/src/routes/monitor/index.tsx b/dashboard/src/routes/monitor/index.tsx index 73d8d4fe..6fd5a071 100644 --- a/dashboard/src/routes/monitor/index.tsx +++ b/dashboard/src/routes/monitor/index.tsx @@ -15,7 +15,7 @@ export function PlannerMonitorPage() {

- MaiSaka 聊天流监控 + 麦麦观察

实时追踪 MaiSaka 推理引擎的完整思考过程 diff --git a/dashboard/src/types/config-schema.ts b/dashboard/src/types/config-schema.ts index 7a9c29b8..d091c04f 100644 --- a/dashboard/src/types/config-schema.ts +++ b/dashboard/src/types/config-schema.ts @@ -22,10 +22,12 @@ export type XWidgetType = | 'switch' | 'textarea' +export type LocalizedText = string | Record + export interface FieldSchema { name: string type: FieldType - label: string + label: LocalizedText description: string required: boolean default?: unknown diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 29d6e976..09eaebcf 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -36,6 +36,11 @@ class BotConfig(ConfigBase): platform: str = Field( default="", json_schema_extra={ + "label": { + "zh_CN": "平台", + "en_US": "Platform", + "ja_JP": "プラットフォーム", + }, "x-widget": "input", "x-icon": "wifi", "x-layout": "inline-right", @@ -48,6 +53,11 @@ class BotConfig(ConfigBase): qq_account: str = Field( default="", json_schema_extra={ + "label": { + "zh_CN": "QQ账号", + "en_US": "QQ account", + "ja_JP": "QQアカウント", + }, "x-widget": "input", "x-icon": "user", "x-layout": "inline-right", @@ -69,6 +79,11 @@ class BotConfig(ConfigBase): nickname: str = Field( default="麦麦", json_schema_extra={ + "label": { + "zh_CN": "机器人昵称", + "en_US": "Bot nickname", + "ja_JP": "ボットのニックネーム", + }, "x-widget": "input", "x-icon": "user-circle", }, @@ -333,6 +348,11 @@ class ChatConfig(ConfigBase): chat_prompts: list["ExtraPromptItem"] = Field( default_factory=lambda: [], json_schema_extra={ + "label": { + "zh_CN": "额外 Prompt", + "en_US": "Extra prompts", + "ja_JP": "追加プロンプト", + }, "x-widget": "custom", "x-icon": "list", }, @@ -341,6 +361,11 @@ class ChatConfig(ConfigBase): enable_talk_value_rules: bool = Field( default=True, json_schema_extra={ + "label": { + "zh_CN": "启用动态发言频率规则", + "en_US": "Enable dynamic talk frequency rules", + "ja_JP": "動的な発言頻度ルールを有効化", + }, "x-widget": "switch", "x-icon": "settings", }, @@ -353,6 +378,11 @@ class ChatConfig(ConfigBase): TalkRulesItem(platform="", item_id="", rule_type="group", time="09:00-18:59", value=1.0), ], json_schema_extra={ + "label": { + "zh_CN": "动态发言频率规则", + "en_US": "Dynamic talk frequency rules", + "ja_JP": "動的な発言頻度ルール", + }, "x-widget": "custom", "x-icon": "list", }, diff --git a/src/webui/config_schema.py b/src/webui/config_schema.py index 9658ec9c..2b02bb56 100644 --- a/src/webui/config_schema.py +++ b/src/webui/config_schema.py @@ -1,12 +1,17 @@ -import inspect from typing import Any, Dict, List, get_args, get_origin from pydantic_core import PydanticUndefined +import inspect + from src.config.config_base import ConfigBase class ConfigSchemaGenerator: + @staticmethod + def _build_label(label: str) -> Dict[str, str]: + return {"zh_CN": label} + @classmethod def generate_schema(cls, config_class: type[ConfigBase], include_nested: bool = True) -> Dict[str, Any]: return cls.generate_config_schema(config_class, include_nested=include_nested) @@ -76,7 +81,7 @@ class ConfigSchemaGenerator: schema: Dict[str, Any] = { "name": field_name, "type": field_type, - "label": field_name, + "label": cls._build_label(field_name), "description": description, "required": field_info.is_required(), }