feat:为webui配置名提供中文翻译,并修改优化布局
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -28,7 +28,6 @@ nonebot-maibot-adapter/
|
|||||||
MaiMBot-LPMM
|
MaiMBot-LPMM
|
||||||
*.zip
|
*.zip
|
||||||
run_bot.bat
|
run_bot.bat
|
||||||
run_na.bat
|
|
||||||
run_all_in_wt.bat
|
run_all_in_wt.bat
|
||||||
run.bat
|
run.bat
|
||||||
log_debug/
|
log_debug/
|
||||||
@@ -37,10 +36,6 @@ run_amds.bat
|
|||||||
run_none.bat
|
run_none.bat
|
||||||
docs-mai/
|
docs-mai/
|
||||||
run.py
|
run.py
|
||||||
message_queue_content.txt
|
|
||||||
message_queue_content.bat
|
|
||||||
message_queue_window.bat
|
|
||||||
message_queue_window.txt
|
|
||||||
queue_update.txt
|
queue_update.txt
|
||||||
start_saka.bat
|
start_saka.bat
|
||||||
.env
|
.env
|
||||||
@@ -50,8 +45,6 @@ start_all.bat
|
|||||||
config/bot_config_dev.toml
|
config/bot_config_dev.toml
|
||||||
config/bot_config.toml
|
config/bot_config.toml
|
||||||
config/bot_config.toml.bak
|
config/bot_config.toml.bak
|
||||||
config/lpmm_config.toml
|
|
||||||
config/lpmm_config.toml.bak
|
|
||||||
template/compare/bot_config_template.toml
|
template/compare/bot_config_template.toml
|
||||||
template/compare/model_config_template.toml
|
template/compare/model_config_template.toml
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "maibot-dashboard",
|
"name": "maibot-dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface DynamicConfigFormProps {
|
|||||||
/** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */
|
/** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */
|
||||||
level?: number
|
level?: number
|
||||||
advancedVisible?: boolean
|
advancedVisible?: boolean
|
||||||
|
sectionColumns?: 1 | 2
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFieldPath(basePath: string, fieldName: string) {
|
function buildFieldPath(basePath: string, fieldName: string) {
|
||||||
@@ -126,6 +127,7 @@ function DynamicConfigSection({
|
|||||||
hooks={hooks}
|
hooks={hooks}
|
||||||
level={level}
|
level={level}
|
||||||
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||||
|
sectionColumns={1}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -150,6 +152,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
hooks = fieldHooks,
|
hooks = fieldHooks,
|
||||||
level = 0,
|
level = 0,
|
||||||
advancedVisible,
|
advancedVisible,
|
||||||
|
sectionColumns = 1,
|
||||||
}) => {
|
}) => {
|
||||||
const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false)
|
const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false)
|
||||||
const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible
|
const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible
|
||||||
@@ -161,10 +164,12 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
|
|
||||||
const renderField = (field: FieldSchema) => {
|
const renderField = (field: FieldSchema) => {
|
||||||
const fieldPath = buildFieldPath(basePath, field.name)
|
const fieldPath = buildFieldPath(basePath, field.name)
|
||||||
|
const nestedSchema = schema.nested?.[field.name]
|
||||||
|
|
||||||
if (hooks.has(fieldPath)) {
|
if (hooks.has(fieldPath)) {
|
||||||
const hookEntry = hooks.get(fieldPath)
|
const hookEntry = hooks.get(fieldPath)
|
||||||
if (!hookEntry) return null
|
if (!hookEntry) return null
|
||||||
|
if (hookEntry.type === 'hidden') return null
|
||||||
|
|
||||||
const HookComponent = hookEntry.component
|
const HookComponent = hookEntry.component
|
||||||
|
|
||||||
@@ -174,7 +179,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
fieldPath={fieldPath}
|
fieldPath={fieldPath}
|
||||||
value={values[field.name]}
|
value={values[field.name]}
|
||||||
onChange={(v) => onChange(field.name, v)}
|
onChange={(v) => onChange(field.name, v)}
|
||||||
|
onParentChange={onChange}
|
||||||
schema={field}
|
schema={field}
|
||||||
|
nestedSchema={nestedSchema}
|
||||||
parentValues={values}
|
parentValues={values}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -185,7 +192,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
fieldPath={fieldPath}
|
fieldPath={fieldPath}
|
||||||
value={values[field.name]}
|
value={values[field.name]}
|
||||||
onChange={(v) => onChange(field.name, v)}
|
onChange={(v) => onChange(field.name, v)}
|
||||||
|
onParentChange={onChange}
|
||||||
schema={field}
|
schema={field}
|
||||||
|
nestedSchema={nestedSchema}
|
||||||
parentValues={values}
|
parentValues={values}
|
||||||
>
|
>
|
||||||
<DynamicField
|
<DynamicField
|
||||||
@@ -208,11 +217,27 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const topLevelFields = schema.fields.filter(
|
const shouldRenderFieldInline = (field: FieldSchema) => {
|
||||||
(field) => !schema.nested?.[field.name],
|
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 normalFields = inlineFields.filter((field) => !field.advanced)
|
||||||
const advancedFields = topLevelFields.filter((field) => field.advanced)
|
const advancedFields = inlineFields.filter((field) => field.advanced)
|
||||||
const visibleFields = resolvedAdvancedVisible
|
const visibleFields = resolvedAdvancedVisible
|
||||||
? [...normalFields, ...advancedFields]
|
? [...normalFields, ...advancedFields]
|
||||||
: normalFields
|
: normalFields
|
||||||
@@ -244,23 +269,32 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderRows = (rows: FieldSchema[][]) => (
|
||||||
|
<>
|
||||||
|
{rows.map((row) => (
|
||||||
|
row.length > 1 ? (
|
||||||
|
<div
|
||||||
|
key={row.map((field) => 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) => (
|
||||||
|
<div key={field.name}>{renderField(field)}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={row[0].name} className="py-1">{renderField(row[0])}</div>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
const renderFieldList = (fields: FieldSchema[]) => (
|
const renderFieldList = (fields: FieldSchema[]) => (
|
||||||
<>
|
<>
|
||||||
{groupFieldsByRow(fields).map((row, index) => (
|
{groupFieldsByRow(fields).map((row, index) => (
|
||||||
<React.Fragment key={row.map((field) => field.name).join('|')}>
|
<React.Fragment key={row.map((field) => field.name).join('|')}>
|
||||||
{index > 0 && <Separator className="my-2 bg-border/50" />}
|
{index > 0 && <Separator className="my-2 bg-border/50" />}
|
||||||
{row.length > 1 ? (
|
{renderRows([row])}
|
||||||
<div
|
|
||||||
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) => (
|
|
||||||
<div key={field.name}>{renderField(field)}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="py-1">{renderField(row[0])}</div>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -268,7 +302,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{topLevelFields.length > 0 && (
|
{inlineFields.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
{advancedVisible === undefined && advancedFields.length > 0 && (
|
{advancedVisible === undefined && advancedFields.length > 0 && (
|
||||||
<div className="flex justify-end pb-2">
|
<div className="flex justify-end pb-2">
|
||||||
@@ -283,7 +317,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{schema.nested &&
|
{schema.nested &&
|
||||||
Object.entries(schema.nested)
|
(() => {
|
||||||
|
const nestedSections = Object.entries(schema.nested)
|
||||||
|
.filter(([key]) => !inlineNestedFieldNames.has(key))
|
||||||
.map(([key, nestedSchema]) => {
|
.map(([key, nestedSchema]) => {
|
||||||
const nestedField = fieldMap.get(key)
|
const nestedField = fieldMap.get(key)
|
||||||
const nestedFieldPath = buildFieldPath(basePath, key)
|
const nestedFieldPath = buildFieldPath(basePath, key)
|
||||||
@@ -299,8 +335,9 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
<HookComponent
|
<HookComponent
|
||||||
fieldPath={nestedFieldPath}
|
fieldPath={nestedFieldPath}
|
||||||
value={values[key]}
|
value={values[key]}
|
||||||
onChange={(v) => onChange(key, v)}
|
onChange={(v) => onChange(key, v)}
|
||||||
schema={nestedField ?? nestedSchema}
|
onParentChange={onChange}
|
||||||
|
schema={nestedField ?? nestedSchema}
|
||||||
nestedSchema={nestedSchema}
|
nestedSchema={nestedSchema}
|
||||||
parentValues={values}
|
parentValues={values}
|
||||||
/>
|
/>
|
||||||
@@ -314,6 +351,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
fieldPath={nestedFieldPath}
|
fieldPath={nestedFieldPath}
|
||||||
value={values[key]}
|
value={values[key]}
|
||||||
onChange={(v) => onChange(key, v)}
|
onChange={(v) => onChange(key, v)}
|
||||||
|
onParentChange={onChange}
|
||||||
schema={nestedField ?? nestedSchema}
|
schema={nestedField ?? nestedSchema}
|
||||||
nestedSchema={nestedSchema}
|
nestedSchema={nestedSchema}
|
||||||
parentValues={values}
|
parentValues={values}
|
||||||
@@ -325,6 +363,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
basePath={nestedFieldPath}
|
basePath={nestedFieldPath}
|
||||||
hooks={hooks}
|
hooks={hooks}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
sectionColumns={1}
|
||||||
/>
|
/>
|
||||||
</HookComponent>
|
</HookComponent>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,11 +415,27 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
basePath={nestedFieldPath}
|
basePath={nestedFieldPath}
|
||||||
hooks={hooks}
|
hooks={hooks}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
sectionColumns={1}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
})}
|
})
|
||||||
|
|
||||||
|
const visibleNestedSections = nestedSections.filter(
|
||||||
|
(section): section is React.ReactElement => Boolean(section),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (level === 0 && sectionColumns === 2 && visibleNestedSections.length > 1) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{visibleNestedSections}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleNestedSections
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LucideIcons from "lucide-react"
|
import * as LucideIcons from "lucide-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { KeyValueEditor } from "@/components/ui/key-value-editor"
|
import { KeyValueEditor } from "@/components/ui/key-value-editor"
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { resolveFieldLabel } from "@/lib/config-label"
|
||||||
import type { FieldSchema } from "@/types/config-schema"
|
import type { FieldSchema } from "@/types/config-schema"
|
||||||
|
|
||||||
export interface DynamicFieldProps {
|
export interface DynamicFieldProps {
|
||||||
@@ -37,6 +39,8 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const fieldLabel = resolveFieldLabel(schema, i18n.language)
|
||||||
const isNumericField = schema.type === 'integer' || schema.type === 'number'
|
const isNumericField = schema.type === 'integer' || schema.type === 'number'
|
||||||
|
|
||||||
const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => {
|
const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => {
|
||||||
@@ -126,17 +130,17 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
|||||||
const inlineDescription = hasOptionDescriptions ? '' : schema.description
|
const inlineDescription = hasOptionDescriptions ? '' : schema.description
|
||||||
|
|
||||||
const renderFieldHeader = () => (
|
const renderFieldHeader = () => (
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
<Label
|
<Label
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex min-h-7 items-center gap-1.5 rounded-md border px-2 py-1 text-sm font-medium shadow-sm",
|
"inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-[15px] font-semibold leading-6",
|
||||||
schema.advanced
|
schema.advanced
|
||||||
? "border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-500/60 dark:bg-amber-500/15 dark:text-amber-100"
|
? "text-amber-800 dark:text-amber-200"
|
||||||
: "bg-muted/60 text-foreground",
|
: "text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
<span className="break-all">{schema.label}</span>
|
<span>{fieldLabel}</span>
|
||||||
{schema.required && <span className="text-destructive">*</span>}
|
{schema.required && <span className="text-destructive">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
{inlineDescription && (
|
{inlineDescription && (
|
||||||
@@ -357,7 +361,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Select value={strValue} onValueChange={(val) => onChange(val)}>
|
<Select value={strValue} onValueChange={(val) => onChange(val)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={`Select ${schema.label}`} />
|
<SelectValue placeholder={`Select ${fieldLabel}`} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{hasOptionDescriptions ? (
|
{hasOptionDescriptions ? (
|
||||||
@@ -415,13 +419,13 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
|||||||
if (supportsInlineRight) {
|
if (supportsInlineRight) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 py-2 sm:flex-row sm:items-center sm:justify-between"
|
className="flex flex-col gap-2 py-2 sm:flex-row sm:items-center"
|
||||||
style={{ '--field-input-width': schema['x-input-width'] ?? '12rem' } as React.CSSProperties}
|
style={{ '--field-input-width': schema['x-input-width'] ?? '12rem' } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="shrink-0">
|
||||||
{renderFieldHeader()}
|
{renderFieldHeader()}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full shrink-0 sm:w-[var(--field-input-width)]">
|
<div className="min-w-20 flex-1 sm:ml-auto sm:max-w-[var(--field-input-width)]">
|
||||||
{renderInputComponent()}
|
{renderInputComponent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
key="settings-sidebar"
|
key="settings-sidebar"
|
||||||
className="relative z-40 hidden shrink-0 lg:block"
|
className="relative z-40 hidden shrink-0 lg:block"
|
||||||
initial={{ width: 0, opacity: 0 }}
|
initial={{ width: 0, opacity: 0 }}
|
||||||
animate={{ width: sidebarOpen ? 256 : 64, opacity: 1 }}
|
animate={{ width: sidebarOpen ? 240 : 64, opacity: 1 }}
|
||||||
exit={{ width: 0, opacity: 0 }}
|
exit={{ width: 0, opacity: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export function Sidebar({
|
|||||||
'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0 lg:h-full',
|
'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0 lg:h-full',
|
||||||
inheritsPageBackground ? 'bg-transparent' : 'bg-card',
|
inheritsPageBackground ? 'bg-transparent' : 'bg-card',
|
||||||
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
|
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
|
||||||
'w-64 lg:w-auto',
|
'w-60 lg:w-auto',
|
||||||
sidebarOpen ? 'lg:w-64' : 'lg:w-16',
|
sidebarOpen ? 'lg:w-60' : 'lg:w-16',
|
||||||
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { Search } from 'lucide-react'
|
import { FileText, Search, SlidersHorizontal } from 'lucide-react'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { LucideProps } from 'lucide-react'
|
import type { LucideProps } from 'lucide-react'
|
||||||
@@ -15,7 +15,10 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { ShortcutKbd } from '@/components/ui/kbd'
|
import { ShortcutKbd } from '@/components/ui/kbd'
|
||||||
import { menuSections } from '@/components/layout/constants'
|
import { menuSections } from '@/components/layout/constants'
|
||||||
import { registeredRoutePaths } from '@/router'
|
import { registeredRoutePaths } from '@/router'
|
||||||
|
import { getBotConfigSchema, getModelConfigSchema } from '@/lib/config-api'
|
||||||
|
import { getAllLocalizedText, resolveFieldLabel } from '@/lib/config-label'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||||
|
|
||||||
interface SearchDialogProps {
|
interface SearchDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -23,19 +26,115 @@ interface SearchDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SearchItem {
|
interface SearchItem {
|
||||||
|
id: string
|
||||||
icon: React.ComponentType<LucideProps>
|
icon: React.ComponentType<LucideProps>
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
path: string
|
path: string
|
||||||
category: string
|
category: string
|
||||||
|
keywords: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSchemaTitle(schema: ConfigSchema, fallback: string) {
|
||||||
|
return schema.uiLabel || schema.classDoc || schema.className || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapConfigSchema(payload: unknown): ConfigSchema | null {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('fields' in payload) {
|
||||||
|
return payload as ConfigSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('schema' in payload) {
|
||||||
|
const schema = (payload as { schema?: unknown }).schema
|
||||||
|
if (schema && typeof schema === 'object' && 'fields' in schema) {
|
||||||
|
return schema as ConfigSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelConfigPath(fieldPath: string) {
|
||||||
|
return fieldPath.startsWith('api_providers') ? '/config/modelProvider' : '/config/model'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFieldSearchText(field: FieldSchema, fieldPath: string, sectionTitle: string, language?: string) {
|
||||||
|
const options = field.options?.join(' ') ?? ''
|
||||||
|
const optionDescriptions = field['x-option-descriptions']
|
||||||
|
? Object.entries(field['x-option-descriptions'])
|
||||||
|
.map(([key, value]) => `${key} ${value}`)
|
||||||
|
.join(' ')
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return [
|
||||||
|
resolveFieldLabel(field, language),
|
||||||
|
...getAllLocalizedText(field.label),
|
||||||
|
field.name,
|
||||||
|
fieldPath,
|
||||||
|
field.description,
|
||||||
|
sectionTitle,
|
||||||
|
field.type,
|
||||||
|
options,
|
||||||
|
optionDescriptions,
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectConfigFields(
|
||||||
|
schema: ConfigSchema,
|
||||||
|
sourceLabel: string,
|
||||||
|
basePath: string,
|
||||||
|
routePath: (fieldPath: string) => string,
|
||||||
|
language?: string,
|
||||||
|
): SearchItem[] {
|
||||||
|
const items: SearchItem[] = []
|
||||||
|
|
||||||
|
const walk = (currentSchema: ConfigSchema, pathPrefix: string, sectionTrail: string[]) => {
|
||||||
|
const sectionTitle = resolveSchemaTitle(currentSchema, sourceLabel)
|
||||||
|
const nextTrail = [...sectionTrail, sectionTitle].filter(Boolean)
|
||||||
|
|
||||||
|
for (const field of currentSchema.fields) {
|
||||||
|
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name
|
||||||
|
const nestedSchema = currentSchema.nested?.[field.name]
|
||||||
|
const fieldTitle = resolveFieldLabel(field, language)
|
||||||
|
const description = field.description || nextTrail.join(' / ') || fieldPath
|
||||||
|
const fullPath = basePath ? `${basePath}.${fieldPath}` : fieldPath
|
||||||
|
const route = routePath(fullPath)
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: `config:${sourceLabel}:${fullPath}`,
|
||||||
|
icon: sourceLabel === '模型配置' ? SlidersHorizontal : FileText,
|
||||||
|
title: fieldTitle,
|
||||||
|
description: `${sourceLabel} / ${nextTrail.join(' / ')} / ${fullPath} · ${description}`,
|
||||||
|
path: route,
|
||||||
|
category: '配置项',
|
||||||
|
keywords: buildFieldSearchText(field, fullPath, nextTrail.join(' / '), language),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nestedSchema) {
|
||||||
|
walk(nestedSchema, fieldPath, nextTrail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(schema, '', [])
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
const [configSearchItems, setConfigSearchItems] = useState<SearchItem[]>([])
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setConfigSearchItems([])
|
||||||
|
}, [i18n.language])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -49,29 +148,91 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
|||||||
return () => window.cancelAnimationFrame(frameId)
|
return () => window.cancelAnimationFrame(frameId)
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || configSearchItems.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const loadConfigSearchItems = async () => {
|
||||||
|
const [botSchemaResult, modelSchemaResult] = await Promise.all([
|
||||||
|
getBotConfigSchema(),
|
||||||
|
getModelConfigSchema(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItems: SearchItem[] = []
|
||||||
|
if (botSchemaResult.success) {
|
||||||
|
const botSchema = unwrapConfigSchema(botSchemaResult.data)
|
||||||
|
if (botSchema) {
|
||||||
|
nextItems.push(...collectConfigFields(
|
||||||
|
botSchema,
|
||||||
|
'Bot 配置',
|
||||||
|
'',
|
||||||
|
() => '/config/bot',
|
||||||
|
i18n.language,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modelSchemaResult.success) {
|
||||||
|
const modelSchema = unwrapConfigSchema(modelSchemaResult.data)
|
||||||
|
if (modelSchema) {
|
||||||
|
nextItems.push(...collectConfigFields(
|
||||||
|
modelSchema,
|
||||||
|
'模型配置',
|
||||||
|
'',
|
||||||
|
getModelConfigPath,
|
||||||
|
i18n.language,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfigSearchItems(nextItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfigSearchItems().catch(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setConfigSearchItems([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [configSearchItems.length, i18n.language, open])
|
||||||
|
|
||||||
const searchItems: SearchItem[] = useMemo(
|
const searchItems: SearchItem[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
menuSections.flatMap((section) =>
|
menuSections.flatMap((section) =>
|
||||||
section.items
|
section.items
|
||||||
.filter((item) => registeredRoutePaths.has(item.path))
|
.filter((item) => registeredRoutePaths.has(item.path))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
|
id: `route:${item.path}`,
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
title: t(item.label),
|
title: t(item.label),
|
||||||
description: item.searchDescription ? t(item.searchDescription) : item.path,
|
description: item.searchDescription ? t(item.searchDescription) : item.path,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
category: t(section.title),
|
category: t(section.title),
|
||||||
|
keywords: [
|
||||||
|
t(item.label),
|
||||||
|
item.path,
|
||||||
|
item.searchDescription ? t(item.searchDescription) : '',
|
||||||
|
t(section.title),
|
||||||
|
].join(' '),
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
[t]
|
[t]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 过滤搜索结果
|
// 过滤搜索结果
|
||||||
const filteredItems = searchItems.filter(
|
const normalizedQuery = searchQuery.trim().toLowerCase()
|
||||||
(item) =>
|
const filteredItems = (normalizedQuery ? [...searchItems, ...configSearchItems] : searchItems)
|
||||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
.filter((item) => item.keywords.toLowerCase().includes(normalizedQuery))
|
||||||
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
.slice(0, 80)
|
||||||
item.category.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
// 导航到页面
|
// 导航到页面
|
||||||
const handleNavigate = useCallback((path: string) => {
|
const handleNavigate = useCallback((path: string) => {
|
||||||
@@ -87,9 +248,11 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
|||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (filteredItems.length === 0) return
|
||||||
setSelectedIndex((prev) => (prev + 1) % filteredItems.length)
|
setSelectedIndex((prev) => (prev + 1) % filteredItems.length)
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (filteredItems.length === 0) return
|
||||||
setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length)
|
setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length)
|
||||||
} else if (e.key === 'Enter' && filteredItems[selectedIndex]) {
|
} else if (e.key === 'Enter' && filteredItems[selectedIndex]) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -128,7 +291,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
|||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.path}
|
key={item.id}
|
||||||
onClick={() => handleNavigate(item.path)}
|
onClick={() => handleNavigate(item.path)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
"botMainConfig": "麦麦主程序配置",
|
"botMainConfig": "麦麦设置",
|
||||||
"aiModelProvider": "AI模型厂商配置",
|
"aiModelProvider": "模型厂商设置",
|
||||||
"modelManagement": "模型管理与分配",
|
"modelManagement": "模型管理与分配",
|
||||||
"promptManagement": "Prompt 管理",
|
"promptManagement": "Prompt 管理",
|
||||||
"adapterConfig": "麦麦适配器配置",
|
"adapterConfig": "麦麦适配器配置",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"pluginConfig": "插件管理",
|
"pluginConfig": "插件管理",
|
||||||
"mcpSettings": "MCP 设置",
|
"mcpSettings": "MCP 设置",
|
||||||
"logViewer": "日志查看器",
|
"logViewer": "日志查看器",
|
||||||
"maisakaMonitor": "MaiSaka 聊天流监控",
|
"maisakaMonitor": "麦麦观察",
|
||||||
"localChat": "本地聊天室",
|
"localChat": "本地聊天室",
|
||||||
"settings": "系统设置"
|
"settings": "系统设置"
|
||||||
}
|
}
|
||||||
@@ -773,9 +773,9 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
"homeDesc": "查看仪表板概览",
|
"homeDesc": "查看仪表板概览",
|
||||||
"botConfig": "麦麦主程序配置",
|
"botConfig": "麦麦设置",
|
||||||
"botConfigDesc": "配置麦麦的核心设置",
|
"botConfigDesc": "配置麦麦的核心设置",
|
||||||
"modelProvider": "麦麦模型提供商配置",
|
"modelProvider": "模型厂商设置",
|
||||||
"modelProviderDesc": "配置模型提供商",
|
"modelProviderDesc": "配置模型提供商",
|
||||||
"model": "麦麦模型配置",
|
"model": "麦麦模型配置",
|
||||||
"modelDesc": "配置模型参数",
|
"modelDesc": "配置模型参数",
|
||||||
|
|||||||
@@ -183,7 +183,7 @@
|
|||||||
--layout-space-lg: 1.5rem;
|
--layout-space-lg: 1.5rem;
|
||||||
--layout-space-xl: 2rem;
|
--layout-space-xl: 2rem;
|
||||||
--layout-space-2xl: 3rem;
|
--layout-space-2xl: 3rem;
|
||||||
--layout-sidebar-width: 16rem;
|
--layout-sidebar-width: 15rem;
|
||||||
--layout-header-height: 3.5rem;
|
--layout-header-height: 3.5rem;
|
||||||
--layout-max-content-width: 1280px;
|
--layout-max-content-width: 1280px;
|
||||||
|
|
||||||
@@ -542,4 +542,4 @@
|
|||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
dashboard/src/lib/config-label.ts
Normal file
51
dashboard/src/lib/config-label.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { FieldSchema, LocalizedText } from '@/types/config-schema'
|
||||||
|
|
||||||
|
const LANGUAGE_ALIASES: Record<string, string[]> = {
|
||||||
|
zh: ['zh_CN', 'zh-CN', 'zh'],
|
||||||
|
en: ['en_US', 'en-US', 'en'],
|
||||||
|
ja: ['ja_JP', 'ja-JP', 'ja'],
|
||||||
|
ko: ['ko_KR', 'ko-KR', 'ko'],
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLanguageCandidates(language?: string) {
|
||||||
|
const normalized = (language || '').replace('-', '_')
|
||||||
|
const baseLanguage = normalized.split('_')[0]
|
||||||
|
return [
|
||||||
|
normalized,
|
||||||
|
language || '',
|
||||||
|
...(LANGUAGE_ALIASES[baseLanguage] ?? []),
|
||||||
|
'zh_CN',
|
||||||
|
'zh-CN',
|
||||||
|
'zh',
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLocalizedText(text: LocalizedText | undefined, language?: string, fallback = '') {
|
||||||
|
if (!text) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
return text || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of getLanguageCandidates(language)) {
|
||||||
|
const value = text[key]
|
||||||
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(text).find(Boolean) ?? fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllLocalizedText(text: LocalizedText | undefined) {
|
||||||
|
if (!text) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return typeof text === 'string' ? [text] : Object.values(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldLabel(field: FieldSchema, language?: string) {
|
||||||
|
return resolveLocalizedText(field.label, language, field.name)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
|||||||
/**
|
/**
|
||||||
* Hook type for field-level customization
|
* Hook type for field-level customization
|
||||||
*/
|
*/
|
||||||
export type FieldHookType = 'replace' | 'wrapper'
|
export type FieldHookType = 'replace' | 'wrapper' | 'hidden'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props passed to a FieldHookComponent
|
* Props passed to a FieldHookComponent
|
||||||
@@ -13,6 +13,7 @@ export interface FieldHookComponentProps {
|
|||||||
fieldPath: string
|
fieldPath: string
|
||||||
value: unknown
|
value: unknown
|
||||||
onChange?: (value: unknown) => void
|
onChange?: (value: unknown) => void
|
||||||
|
onParentChange?: (field: string, value: unknown) => void
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
schema?: ConfigSchema | FieldSchema
|
schema?: ConfigSchema | FieldSchema
|
||||||
parentValues?: Record<string, unknown>
|
parentValues?: Record<string, unknown>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 修改此处的版本号后,所有展示版本的地方都会自动更新
|
* 修改此处的版本号后,所有展示版本的地方都会自动更新
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const APP_VERSION = '1.0.5'
|
export const APP_VERSION = '1.0.6'
|
||||||
export const APP_NAME = 'MaiBot Dashboard'
|
export const APP_NAME = 'MaiBot Dashboard'
|
||||||
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`
|
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ import { ChevronDown, ChevronUp, Code2, Info, Layout, Power, RefreshCw, Save } f
|
|||||||
|
|
||||||
import type { ConfigSchema } from '@/types/config-schema'
|
import type { ConfigSchema } from '@/types/config-schema'
|
||||||
import {
|
import {
|
||||||
BotPlatformsHook,
|
BotPlatformAccountsHook,
|
||||||
ChatPromptsHook,
|
ChatPromptsHook,
|
||||||
ChatTalkValueRulesHook,
|
ChatTalkValueRulesHook,
|
||||||
ExpressionGroupsHook,
|
ExpressionGroupsHook,
|
||||||
ExpressionLearningListHook,
|
ExpressionLearningListHook,
|
||||||
KeywordRulesHook,
|
KeywordRulesHook,
|
||||||
|
HiddenFieldHook,
|
||||||
MCPRootItemsHook,
|
MCPRootItemsHook,
|
||||||
MCPServersHook,
|
MCPServersHook,
|
||||||
RegexRulesHook,
|
RegexRulesHook,
|
||||||
@@ -415,7 +416,9 @@ function BotConfigPageContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hookEntries = [
|
const hookEntries = [
|
||||||
['bot.platforms', BotPlatformsHook],
|
['bot.platform', BotPlatformAccountsHook, 'replace'],
|
||||||
|
['bot.qq_account', HiddenFieldHook, 'hidden'],
|
||||||
|
['bot.platforms', HiddenFieldHook, 'hidden'],
|
||||||
['chat.chat_prompts', ChatPromptsHook],
|
['chat.chat_prompts', ChatPromptsHook],
|
||||||
['chat.talk_value_rules', ChatTalkValueRulesHook],
|
['chat.talk_value_rules', ChatTalkValueRulesHook],
|
||||||
['expression.expression_groups', ExpressionGroupsHook],
|
['expression.expression_groups', ExpressionGroupsHook],
|
||||||
@@ -426,8 +429,8 @@ function BotConfigPageContent() {
|
|||||||
['mcp.servers', MCPServersHook],
|
['mcp.servers', MCPServersHook],
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
for (const [fieldPath, hookComponent] of hookEntries) {
|
for (const [fieldPath, hookComponent, hookType = 'replace'] of hookEntries) {
|
||||||
fieldHooks.register(fieldPath, hookComponent, 'replace')
|
fieldHooks.register(fieldPath, hookComponent, hookType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -596,7 +599,7 @@ function BotConfigPageContent() {
|
|||||||
setHasUnsavedChanges(false)
|
setHasUnsavedChanges(false)
|
||||||
toast({
|
toast({
|
||||||
title: '保存成功',
|
title: '保存成功',
|
||||||
description: '麦麦主程序配置已保存',
|
description: '麦麦设置已保存',
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存配置失败:', error)
|
console.error('保存配置失败:', error)
|
||||||
@@ -772,7 +775,7 @@ function BotConfigPageContent() {
|
|||||||
<div className="flex flex-col gap-3 sm:gap-4">
|
<div className="flex flex-col gap-3 sm:gap-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold">麦麦主程序配置</h1>
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold">麦麦设置</h1>
|
||||||
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">管理麦麦的核心功能和行为设置</p>
|
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">管理麦麦的核心功能和行为设置</p>
|
||||||
</div>
|
</div>
|
||||||
{/* 按钮组 - 桌面端靠右 */}
|
{/* 按钮组 - 桌面端靠右 */}
|
||||||
@@ -1032,6 +1035,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
|||||||
setHasUnsavedChanges(true)
|
setHasUnsavedChanges(true)
|
||||||
}}
|
}}
|
||||||
hooks={fieldHooks}
|
hooks={fieldHooks}
|
||||||
|
sectionColumns={2}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { resolveLocalizedText } from '@/lib/config-label'
|
||||||
import type { FieldHookComponent } from '@/lib/field-hooks'
|
import type { FieldHookComponent } from '@/lib/field-hooks'
|
||||||
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string):
|
|||||||
return fieldPath?.split('.').at(-1) || 'JSON 配置'
|
return fieldPath?.split('.').at(-1) || 'JSON 配置'
|
||||||
}
|
}
|
||||||
if ('label' in schema && schema.label) {
|
if ('label' in schema && schema.label) {
|
||||||
return schema.label
|
return resolveLocalizedText(schema.label, undefined, fieldPath?.split('.').at(-1) || 'JSON 配置')
|
||||||
}
|
}
|
||||||
if ('uiLabel' in schema && schema.uiLabel) {
|
if ('uiLabel' in schema && schema.uiLabel) {
|
||||||
return schema.uiLabel
|
return schema.uiLabel
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { DynamicConfigForm } from '@/components/dynamic-form/DynamicConfigForm'
|
import { DynamicConfigForm } from '@/components/dynamic-form/DynamicConfigForm'
|
||||||
|
import { resolveLocalizedText } from '@/lib/config-label'
|
||||||
import type { FieldHookComponent } from '@/lib/field-hooks'
|
import type { FieldHookComponent } from '@/lib/field-hooks'
|
||||||
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string):
|
|||||||
return fieldPath?.split('.').at(-1) ?? '列表配置'
|
return fieldPath?.split('.').at(-1) ?? '列表配置'
|
||||||
}
|
}
|
||||||
if ('label' in schema && schema.label) {
|
if ('label' in schema && schema.label) {
|
||||||
return schema.label
|
return resolveLocalizedText(schema.label, undefined, fieldPath?.split('.').at(-1) ?? '列表配置')
|
||||||
}
|
}
|
||||||
if ('uiLabel' in schema && schema.uiLabel) {
|
if ('uiLabel' in schema && schema.uiLabel) {
|
||||||
return schema.uiLabel
|
return schema.uiLabel
|
||||||
|
|||||||
@@ -294,6 +294,120 @@ export const BotPlatformsHook: FieldHookComponent = ({ onChange, value }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const HiddenFieldHook: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
export const BotPlatformAccountsHook: FieldHookComponent = ({
|
||||||
|
onChange,
|
||||||
|
onParentChange,
|
||||||
|
parentValues,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
const primaryPlatform = typeof value === 'string' ? value : ''
|
||||||
|
const qqAccountValue = parentValues?.qq_account
|
||||||
|
const qqAccount =
|
||||||
|
typeof qqAccountValue === 'string' || typeof qqAccountValue === 'number'
|
||||||
|
? String(qqAccountValue)
|
||||||
|
: ''
|
||||||
|
const platforms = normalizePlatformAccounts(parentValues?.platforms)
|
||||||
|
const rows = platforms.map(parsePlatformAccount)
|
||||||
|
|
||||||
|
const updateRows = (nextRows: PlatformAccountRow[]) => {
|
||||||
|
onParentChange?.('platforms', nextRows.map(formatPlatformAccount))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRow = () => {
|
||||||
|
updateRows([...rows, { platform: '', account: '' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRow = (rowIndex: number) => {
|
||||||
|
updateRows(rows.filter((_, index) => index !== rowIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRow = (rowIndex: number, patch: Partial<PlatformAccountRow>) => {
|
||||||
|
updateRows(rows.map((row, index) => (index === rowIndex ? { ...row, ...patch } : row)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[15px] font-semibold leading-6">平台账号</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
第一行为主平台,固定保存到 platform 与 qq_account;其他行保存到 platforms。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" size="sm" variant="outline" onClick={addRow}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
添加平台
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_2.5rem]">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">平台</Label>
|
||||||
|
<Input
|
||||||
|
value={primaryPlatform}
|
||||||
|
placeholder="qq"
|
||||||
|
onChange={(event) => onChange?.(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">账号</Label>
|
||||||
|
<Input
|
||||||
|
className="font-mono"
|
||||||
|
value={qqAccount}
|
||||||
|
placeholder="2814567326"
|
||||||
|
onChange={(event) => onParentChange?.('qq_account', event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-end">
|
||||||
|
<span className="rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||||
|
主
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rows.map((row, rowIndex) => (
|
||||||
|
<div
|
||||||
|
key={rowIndex}
|
||||||
|
className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_2.5rem]"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">平台</Label>
|
||||||
|
<Input
|
||||||
|
value={row.platform}
|
||||||
|
placeholder="wx"
|
||||||
|
onChange={(event) => updateRow(rowIndex, { platform: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">账号</Label>
|
||||||
|
<Input
|
||||||
|
className="font-mono"
|
||||||
|
value={row.account}
|
||||||
|
placeholder="114514"
|
||||||
|
onChange={(event) => updateRow(rowIndex, { account: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={`删除其他平台 ${rowIndex + 1}`}
|
||||||
|
onClick={() => removeRow(rowIndex)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const KeywordRulesHook = createListItemEditorHook({
|
export const KeywordRulesHook = createListItemEditorHook({
|
||||||
addLabel: '添加关键词规则',
|
addLabel: '添加关键词规则',
|
||||||
helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。',
|
helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。',
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export type {
|
|||||||
} from './useAutoSave'
|
} from './useAutoSave'
|
||||||
export {
|
export {
|
||||||
BotPlatformsHook,
|
BotPlatformsHook,
|
||||||
|
BotPlatformAccountsHook,
|
||||||
ChatPromptsHook,
|
ChatPromptsHook,
|
||||||
ChatTalkValueRulesHook,
|
ChatTalkValueRulesHook,
|
||||||
ExpressionGroupsHook,
|
ExpressionGroupsHook,
|
||||||
ExpressionLearningListHook,
|
ExpressionLearningListHook,
|
||||||
KeywordRulesHook,
|
KeywordRulesHook,
|
||||||
|
HiddenFieldHook,
|
||||||
MCPRootItemsHook,
|
MCPRootItemsHook,
|
||||||
MCPServersHook,
|
MCPServersHook,
|
||||||
RegexRulesHook,
|
RegexRulesHook,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, type MouseEvent } from 'react'
|
import { useState, useEffect, useCallback, useRef, type MouseEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@@ -47,8 +48,9 @@ import {
|
|||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { getModelConfig, getModelConfigSchema, updateModelConfig } from '@/lib/config-api'
|
||||||
|
import { resolveFieldLabel } from '@/lib/config-label'
|
||||||
import type { ConfigSchema } from '@/types/config-schema'
|
import type { ConfigSchema } from '@/types/config-schema'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
@@ -82,6 +84,7 @@ export function ModelConfigPage() {
|
|||||||
|
|
||||||
// 内部实现组件
|
// 内部实现组件
|
||||||
function ModelConfigPageContent() {
|
function ModelConfigPageContent() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
const [models, setModels] = useState<ModelInfo[]>([])
|
const [models, setModels] = useState<ModelInfo[]>([])
|
||||||
const [providers, setProviders] = useState<string[]>([])
|
const [providers, setProviders] = useState<string[]>([])
|
||||||
const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([])
|
const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([])
|
||||||
@@ -105,7 +108,6 @@ function ModelConfigPageContent() {
|
|||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [jumpToPage, setJumpToPage] = useState('')
|
const [jumpToPage, setJumpToPage] = useState('')
|
||||||
|
|
||||||
const [advancedTemperatureMode, setAdvancedTemperatureMode] = useState(false)
|
|
||||||
const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false)
|
const [advancedModelSettingsVisible, setAdvancedModelSettingsVisible] = useState(false)
|
||||||
const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false)
|
const [advancedTaskSettingsVisible, setAdvancedTaskSettingsVisible] = useState(false)
|
||||||
const [restartNoticeVisible, setRestartNoticeVisible] = useState(
|
const [restartNoticeVisible, setRestartNoticeVisible] = useState(
|
||||||
@@ -1009,15 +1011,11 @@ function ModelConfigPageContent() {
|
|||||||
{taskConfigSchema.fields
|
{taskConfigSchema.fields
|
||||||
.filter(f => f.type === 'object' && (advancedTaskSettingsVisible || !f.advanced))
|
.filter(f => f.type === 'object' && (advancedTaskSettingsVisible || !f.advanced))
|
||||||
.map((field, index) => {
|
.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 (
|
return (
|
||||||
<TaskConfigCard
|
<TaskConfigCard
|
||||||
key={field.name}
|
key={field.name}
|
||||||
title={`${title} (${field.name})`}
|
title={resolveFieldLabel(field, i18n.language)}
|
||||||
description={subtitle}
|
description={field.description}
|
||||||
taskConfig={taskConfig[field.name] ?? { model_list: [] }}
|
taskConfig={taskConfig[field.name] ?? { model_list: [] }}
|
||||||
modelNames={modelNames}
|
modelNames={modelNames}
|
||||||
onChange={(f, value) => updateTaskConfig(field.name, f, value)}
|
onChange={(f, value) => updateTaskConfig(field.name, f, value)}
|
||||||
@@ -1425,7 +1423,7 @@ function ModelConfigPageContent() {
|
|||||||
checked={editingModel?.temperature != null}
|
checked={editingModel?.temperature != null}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setEditingModel((prev) => prev ? { ...prev, temperature: 0.5 } : null)
|
setEditingModel((prev) => prev ? { ...prev, temperature: 0.7 } : null)
|
||||||
} else {
|
} else {
|
||||||
setEditingModel((prev) => prev ? { ...prev, temperature: null } : null)
|
setEditingModel((prev) => prev ? { ...prev, temperature: null } : null)
|
||||||
}
|
}
|
||||||
@@ -1437,43 +1435,28 @@ function ModelConfigPageContent() {
|
|||||||
<div className="space-y-3 pt-2 border-t">
|
<div className="space-y-3 pt-2 border-t">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Label className="text-sm">温度值</Label>
|
<Label className="text-sm">温度值</Label>
|
||||||
<div className="flex items-center gap-2">
|
<Input
|
||||||
<Input
|
type="number"
|
||||||
type="number"
|
value={editingModel.temperature}
|
||||||
value={editingModel.temperature}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
const value = parseFloat(e.target.value)
|
||||||
const value = parseFloat(e.target.value)
|
if (!isNaN(value) && value >= 0 && value <= 2) {
|
||||||
if (!isNaN(value) && value >= 0 && value <= 2) {
|
setEditingModel((prev) => prev ? { ...prev, temperature: value } : null)
|
||||||
setEditingModel((prev) => prev ? { ...prev, temperature: value } : null)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onBlur={(e) => {
|
||||||
onBlur={(e) => {
|
const value = parseFloat(e.target.value)
|
||||||
const value = parseFloat(e.target.value)
|
if (isNaN(value) || value < 0) {
|
||||||
if (isNaN(value) || value < 0) {
|
setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null)
|
||||||
setEditingModel((prev) => prev ? { ...prev, temperature: 0 } : null)
|
} else if (value > 2) {
|
||||||
} else if (value > 2) {
|
setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null)
|
||||||
setEditingModel((prev) => prev ? { ...prev, temperature: 2 } : null)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
step={0.01}
|
||||||
step={0.01}
|
min={0}
|
||||||
min={0}
|
max={2}
|
||||||
max={2}
|
className="w-20 h-8 text-sm text-right tabular-nums"
|
||||||
className="w-20 h-8 text-sm text-right tabular-nums"
|
/>
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setAdvancedTemperatureMode(!advancedTemperatureMode)}
|
|
||||||
className="h-8 px-2"
|
|
||||||
title={advancedTemperatureMode ? "切换到基础模式 (0-1)" : "解锁高级范围 (0-2)"}
|
|
||||||
>
|
|
||||||
{advancedTemperatureMode ? (
|
|
||||||
<Unlock className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Lock className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs text-muted-foreground tabular-nums">0</span>
|
<span className="text-xs text-muted-foreground tabular-nums">0</span>
|
||||||
@@ -1485,25 +1468,22 @@ function ModelConfigPageContent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
min={0}
|
min={0}
|
||||||
max={advancedTemperatureMode ? 2 : 1}
|
max={2}
|
||||||
step={advancedTemperatureMode ? 0.05 : 0.1}
|
step={0.05}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground tabular-nums">{advancedTemperatureMode ? '2' : '1'}</span>
|
<span className="text-xs text-muted-foreground tabular-nums">2</span>
|
||||||
</div>
|
</div>
|
||||||
{advancedTemperatureMode && (
|
{editingModel.temperature > 1 && (
|
||||||
<Alert className="bg-amber-500/10 border-amber-500/20 [&>svg+div]:translate-y-0">
|
<Alert className="bg-amber-500/10 border-amber-500/20 [&>svg+div]:translate-y-0">
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||||
<AlertDescription className="text-xs text-amber-600 dark:text-amber-400">
|
<AlertDescription className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
高级模式:温度 > 1 会产生更随机、更不可预测的输出,请谨慎使用
|
温度 > 1 会产生更随机、更不可预测的输出,请谨慎使用
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{advancedTemperatureMode
|
较低(0.1-0.5)产生确定输出,中等(0.5-1.0)平衡创造性,较高(1.0-2.0)产生极度随机输出
|
||||||
? "较低(0.1-0.5)产生确定输出,中等(0.5-1.0)平衡创造性,较高(1.0-2.0)产生极度随机输出"
|
|
||||||
: "较低的温度(0.1-0.3)产生更确定的输出,较高的温度(0.7-1.0)产生更多样化的输出"
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({
|
|||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="2"
|
||||||
value={taskConfig.temperature ?? 0.3}
|
value={taskConfig.temperature ?? 0.7}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = parseFloat(e.target.value)
|
const value = parseFloat(e.target.value)
|
||||||
if (!isNaN(value) && value >= 0 && value <= 1) {
|
if (!isNaN(value) && value >= 0 && value <= 2) {
|
||||||
onChange('temperature', value)
|
onChange('temperature', value)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -116,10 +116,10 @@ export const TaskConfigCard = React.memo(function TaskConfigCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
value={[taskConfig.temperature ?? 0.3]}
|
value={[taskConfig.temperature ?? 0.7]}
|
||||||
onValueChange={(values) => onChange('temperature', values[0])}
|
onValueChange={(values) => onChange('temperature', values[0])}
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -742,7 +742,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold">AI模型厂商配置</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">模型厂商设置</h1>
|
||||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">管理 AI 模型厂商的 API 配置</p>
|
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">管理 AI 模型厂商的 API 配置</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function PlannerMonitorPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2">
|
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2">
|
||||||
<Activity className="h-6 w-6 sm:h-7 sm:w-7" />
|
<Activity className="h-6 w-6 sm:h-7 sm:w-7" />
|
||||||
MaiSaka 聊天流监控
|
麦麦观察
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
|
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
|
||||||
实时追踪 MaiSaka 推理引擎的完整思考过程
|
实时追踪 MaiSaka 推理引擎的完整思考过程
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export type XWidgetType =
|
|||||||
| 'switch'
|
| 'switch'
|
||||||
| 'textarea'
|
| 'textarea'
|
||||||
|
|
||||||
|
export type LocalizedText = string | Record<string, string>
|
||||||
|
|
||||||
export interface FieldSchema {
|
export interface FieldSchema {
|
||||||
name: string
|
name: string
|
||||||
type: FieldType
|
type: FieldType
|
||||||
label: string
|
label: LocalizedText
|
||||||
description: string
|
description: string
|
||||||
required: boolean
|
required: boolean
|
||||||
default?: unknown
|
default?: unknown
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ class BotConfig(ConfigBase):
|
|||||||
platform: str = Field(
|
platform: str = Field(
|
||||||
default="",
|
default="",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "平台",
|
||||||
|
"en_US": "Platform",
|
||||||
|
"ja_JP": "プラットフォーム",
|
||||||
|
},
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "wifi",
|
"x-icon": "wifi",
|
||||||
"x-layout": "inline-right",
|
"x-layout": "inline-right",
|
||||||
@@ -48,6 +53,11 @@ class BotConfig(ConfigBase):
|
|||||||
qq_account: str = Field(
|
qq_account: str = Field(
|
||||||
default="",
|
default="",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "QQ账号",
|
||||||
|
"en_US": "QQ account",
|
||||||
|
"ja_JP": "QQアカウント",
|
||||||
|
},
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "user",
|
"x-icon": "user",
|
||||||
"x-layout": "inline-right",
|
"x-layout": "inline-right",
|
||||||
@@ -69,6 +79,11 @@ class BotConfig(ConfigBase):
|
|||||||
nickname: str = Field(
|
nickname: str = Field(
|
||||||
default="麦麦",
|
default="麦麦",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "机器人昵称",
|
||||||
|
"en_US": "Bot nickname",
|
||||||
|
"ja_JP": "ボットのニックネーム",
|
||||||
|
},
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "user-circle",
|
"x-icon": "user-circle",
|
||||||
},
|
},
|
||||||
@@ -333,6 +348,11 @@ class ChatConfig(ConfigBase):
|
|||||||
chat_prompts: list["ExtraPromptItem"] = Field(
|
chat_prompts: list["ExtraPromptItem"] = Field(
|
||||||
default_factory=lambda: [],
|
default_factory=lambda: [],
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "额外 Prompt",
|
||||||
|
"en_US": "Extra prompts",
|
||||||
|
"ja_JP": "追加プロンプト",
|
||||||
|
},
|
||||||
"x-widget": "custom",
|
"x-widget": "custom",
|
||||||
"x-icon": "list",
|
"x-icon": "list",
|
||||||
},
|
},
|
||||||
@@ -341,6 +361,11 @@ class ChatConfig(ConfigBase):
|
|||||||
enable_talk_value_rules: bool = Field(
|
enable_talk_value_rules: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "启用动态发言频率规则",
|
||||||
|
"en_US": "Enable dynamic talk frequency rules",
|
||||||
|
"ja_JP": "動的な発言頻度ルールを有効化",
|
||||||
|
},
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "settings",
|
"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),
|
TalkRulesItem(platform="", item_id="", rule_type="group", time="09:00-18:59", value=1.0),
|
||||||
],
|
],
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": {
|
||||||
|
"zh_CN": "动态发言频率规则",
|
||||||
|
"en_US": "Dynamic talk frequency rules",
|
||||||
|
"ja_JP": "動的な発言頻度ルール",
|
||||||
|
},
|
||||||
"x-widget": "custom",
|
"x-widget": "custom",
|
||||||
"x-icon": "list",
|
"x-icon": "list",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import inspect
|
|
||||||
from typing import Any, Dict, List, get_args, get_origin
|
from typing import Any, Dict, List, get_args, get_origin
|
||||||
|
|
||||||
from pydantic_core import PydanticUndefined
|
from pydantic_core import PydanticUndefined
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
from src.config.config_base import ConfigBase
|
from src.config.config_base import ConfigBase
|
||||||
|
|
||||||
|
|
||||||
class ConfigSchemaGenerator:
|
class ConfigSchemaGenerator:
|
||||||
|
@staticmethod
|
||||||
|
def _build_label(label: str) -> Dict[str, str]:
|
||||||
|
return {"zh_CN": label}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_schema(cls, config_class: type[ConfigBase], include_nested: bool = True) -> Dict[str, Any]:
|
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)
|
return cls.generate_config_schema(config_class, include_nested=include_nested)
|
||||||
@@ -76,7 +81,7 @@ class ConfigSchemaGenerator:
|
|||||||
schema: Dict[str, Any] = {
|
schema: Dict[str, Any] = {
|
||||||
"name": field_name,
|
"name": field_name,
|
||||||
"type": field_type,
|
"type": field_type,
|
||||||
"label": field_name,
|
"label": cls._build_label(field_name),
|
||||||
"description": description,
|
"description": description,
|
||||||
"required": field_info.is_required(),
|
"required": field_info.is_required(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user