feat:优化webui多个页面的人机交互,修复插件地址问题,放宽插件id限制,增加高级页面缩进,统计页面快捷按钮,优化新手引导
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as LucideIcons from 'lucide-react'
|
import * as LucideIcons from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,8 +10,8 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
|
||||||
import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks'
|
import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks'
|
||||||
|
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
|
||||||
|
|
||||||
import { DynamicField } from './DynamicField'
|
import { DynamicField } from './DynamicField'
|
||||||
|
|
||||||
@@ -20,53 +21,142 @@ export interface DynamicConfigFormProps {
|
|||||||
onChange: (field: string, value: unknown) => void
|
onChange: (field: string, value: unknown) => void
|
||||||
basePath?: string
|
basePath?: string
|
||||||
hooks?: FieldHookRegistry
|
hooks?: FieldHookRegistry
|
||||||
/** 嵌套层级:0 = tab 内容层, 1 = section 内容层, 2+ = 更深嵌套 */
|
/** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */
|
||||||
level?: number
|
level?: number
|
||||||
|
advancedVisible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFieldPath(basePath: string, fieldName: string) {
|
||||||
|
return basePath ? `${basePath}.${fieldName}` : fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTopLevelAdvancedFields(schema: ConfigSchema) {
|
||||||
|
return schema.fields.some((field) => field.advanced && !schema.nested?.[field.name])
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionIcon({ iconName }: { iconName?: string }) {
|
||||||
|
if (!iconName) return null
|
||||||
|
const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as
|
||||||
|
| React.ComponentType<{ className?: string }>
|
||||||
|
| undefined
|
||||||
|
if (!IconComponent) return null
|
||||||
|
return <IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdvancedSettingsButton({
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={active ? 'secondary' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
高级设置
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DynamicConfigSection({
|
||||||
|
basePath,
|
||||||
|
hooks,
|
||||||
|
level,
|
||||||
|
nestedSchema,
|
||||||
|
onChange,
|
||||||
|
sectionDescription,
|
||||||
|
sectionTitle,
|
||||||
|
values,
|
||||||
|
}: {
|
||||||
|
basePath: string
|
||||||
|
hooks: FieldHookRegistry
|
||||||
|
level: number
|
||||||
|
nestedSchema: ConfigSchema
|
||||||
|
onChange: (field: string, value: unknown) => void
|
||||||
|
sectionDescription?: string
|
||||||
|
sectionTitle: string
|
||||||
|
values: Record<string, unknown>
|
||||||
|
}) {
|
||||||
|
const [advancedVisible, setAdvancedVisible] = React.useState(false)
|
||||||
|
const hasAdvanced = hasTopLevelAdvancedFields(nestedSchema)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SectionIcon iconName={nestedSchema.uiIcon} />
|
||||||
|
<CardTitle className="text-lg">{sectionTitle}</CardTitle>
|
||||||
|
</div>
|
||||||
|
{sectionDescription && (
|
||||||
|
<CardDescription>{sectionDescription}</CardDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hasAdvanced && (
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
active={advancedVisible}
|
||||||
|
onClick={() => setAdvancedVisible((current) => !current)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DynamicConfigForm
|
||||||
|
schema={nestedSchema}
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
basePath={basePath}
|
||||||
|
hooks={hooks}
|
||||||
|
level={level}
|
||||||
|
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DynamicConfigForm - 动态配置表单组件
|
* DynamicConfigForm - 动态配置表单组件
|
||||||
*
|
*
|
||||||
* 根据 ConfigSchema 渲染表单字段,支持:
|
* 根据 ConfigSchema 渲染表单字段,支持:
|
||||||
* 1. Hook 系统:通过 FieldHookRegistry 自定义字段渲染
|
* 1. Hook 系统:通过 FieldHookRegistry 自定义字段渲染
|
||||||
* - replace 模式:完全替换默认渲染
|
* - replace 模式:完全替换默认渲染
|
||||||
* - wrapper 模式:包装默认渲染(通过 children 传递)
|
* - wrapper 模式:包装默认渲染(通过 children 传递)
|
||||||
* 2. 嵌套 schema:递归渲染 schema.nested 中的子配置,使用 Card 容器区分层级
|
* 2. 嵌套 schema:递归渲染 schema.nested 中的子配置
|
||||||
* 3. 默认渲染:使用 DynamicField 组件
|
* 3. 高级设置:由栏目标题右侧按钮控制显示
|
||||||
*/
|
*/
|
||||||
export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||||
schema,
|
schema,
|
||||||
values,
|
values,
|
||||||
onChange,
|
onChange,
|
||||||
basePath = '',
|
basePath = '',
|
||||||
hooks = fieldHooks, // 默认使用全局单例
|
hooks = fieldHooks,
|
||||||
level = 0,
|
level = 0,
|
||||||
|
advancedVisible,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [localAdvancedVisible, setLocalAdvancedVisible] = React.useState(false)
|
||||||
|
const resolvedAdvancedVisible = advancedVisible ?? localAdvancedVisible
|
||||||
|
|
||||||
const fieldMap = React.useMemo(
|
const fieldMap = React.useMemo(
|
||||||
() => new Map(schema.fields.map((field) => [field.name, field])),
|
() => new Map(schema.fields.map((field) => [field.name, field])),
|
||||||
[schema.fields]
|
[schema.fields],
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildFieldPath = (fieldName: string) => {
|
|
||||||
return basePath ? `${basePath}.${fieldName}` : fieldName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染单个字段
|
|
||||||
* 检查是否有注册的 Hook,根据 Hook 类型选择渲染方式
|
|
||||||
*/
|
|
||||||
const renderField = (field: FieldSchema) => {
|
const renderField = (field: FieldSchema) => {
|
||||||
const fieldPath = buildFieldPath(field.name)
|
const fieldPath = buildFieldPath(basePath, field.name)
|
||||||
|
|
||||||
// 检查是否有注册的 Hook
|
|
||||||
if (hooks.has(fieldPath)) {
|
if (hooks.has(fieldPath)) {
|
||||||
const hookEntry = hooks.get(fieldPath)
|
const hookEntry = hooks.get(fieldPath)
|
||||||
if (!hookEntry) return null // Type guard(理论上不会发生)
|
if (!hookEntry) return null
|
||||||
|
|
||||||
const HookComponent = hookEntry.component
|
const HookComponent = hookEntry.component
|
||||||
|
|
||||||
if (hookEntry.type === 'replace') {
|
if (hookEntry.type === 'replace') {
|
||||||
// replace 模式:完全替换默认渲染
|
|
||||||
return (
|
return (
|
||||||
<HookComponent
|
<HookComponent
|
||||||
fieldPath={fieldPath}
|
fieldPath={fieldPath}
|
||||||
@@ -75,27 +165,25 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
schema={field}
|
schema={field}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
// wrapper 模式:包装默认渲染
|
|
||||||
return (
|
return (
|
||||||
<HookComponent
|
<HookComponent
|
||||||
fieldPath={fieldPath}
|
fieldPath={fieldPath}
|
||||||
|
value={values[field.name]}
|
||||||
|
onChange={(v) => onChange(field.name, v)}
|
||||||
|
schema={field}
|
||||||
|
>
|
||||||
|
<DynamicField
|
||||||
|
schema={field}
|
||||||
value={values[field.name]}
|
value={values[field.name]}
|
||||||
onChange={(v) => onChange(field.name, v)}
|
onChange={(v) => onChange(field.name, v)}
|
||||||
schema={field}
|
fieldPath={fieldPath}
|
||||||
>
|
/>
|
||||||
<DynamicField
|
</HookComponent>
|
||||||
schema={field}
|
)
|
||||||
value={values[field.name]}
|
|
||||||
onChange={(v) => onChange(field.name, v)}
|
|
||||||
fieldPath={fieldPath}
|
|
||||||
/>
|
|
||||||
</HookComponent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无 Hook,使用默认渲染
|
|
||||||
return (
|
return (
|
||||||
<DynamicField
|
<DynamicField
|
||||||
schema={field}
|
schema={field}
|
||||||
@@ -106,44 +194,49 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染 section 图标 */
|
|
||||||
const renderSectionIcon = (iconName?: string) => {
|
|
||||||
if (!iconName) return null
|
|
||||||
const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as
|
|
||||||
| React.ComponentType<{ className?: string }>
|
|
||||||
| undefined
|
|
||||||
if (!IconComponent) return null
|
|
||||||
return <IconComponent className="h-5 w-5 text-muted-foreground" />
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤出不属于 nested 的顶层字段
|
|
||||||
const topLevelFields = schema.fields.filter(
|
const topLevelFields = schema.fields.filter(
|
||||||
(field) => !schema.nested?.[field.name]
|
(field) => !schema.nested?.[field.name],
|
||||||
|
)
|
||||||
|
const normalFields = topLevelFields.filter((field) => !field.advanced)
|
||||||
|
const advancedFields = topLevelFields.filter((field) => field.advanced)
|
||||||
|
const visibleFields = resolvedAdvancedVisible
|
||||||
|
? [...normalFields, ...advancedFields]
|
||||||
|
: normalFields
|
||||||
|
|
||||||
|
const renderFieldList = (fields: FieldSchema[]) => (
|
||||||
|
<>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<React.Fragment key={field.name}>
|
||||||
|
{index > 0 && field.type !== 'boolean' && fields[index - 1]?.type !== 'boolean' && (
|
||||||
|
<Separator className="my-1" />
|
||||||
|
)}
|
||||||
|
<div>{renderField(field)}</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 渲染顶层字段 */}
|
|
||||||
{topLevelFields.length > 0 && (
|
{topLevelFields.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{topLevelFields.map((field, index) => (
|
{advancedVisible === undefined && advancedFields.length > 0 && (
|
||||||
<React.Fragment key={field.name}>
|
<div className="flex justify-end pb-2">
|
||||||
{index > 0 && field.type !== 'boolean' && topLevelFields[index - 1]?.type !== 'boolean' && (
|
<AdvancedSettingsButton
|
||||||
<Separator className="my-1" />
|
active={localAdvancedVisible}
|
||||||
)}
|
onClick={() => setLocalAdvancedVisible((current) => !current)}
|
||||||
<div>{renderField(field)}</div>
|
/>
|
||||||
</React.Fragment>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
{renderFieldList(visibleFields)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 渲染嵌套 schema */}
|
|
||||||
{schema.nested &&
|
{schema.nested &&
|
||||||
Object.entries(schema.nested).map(([key, nestedSchema]) => {
|
Object.entries(schema.nested).map(([key, nestedSchema]) => {
|
||||||
const nestedField = fieldMap.get(key)
|
const nestedField = fieldMap.get(key)
|
||||||
const nestedFieldPath = buildFieldPath(key)
|
const nestedFieldPath = buildFieldPath(basePath, key)
|
||||||
|
|
||||||
// Hook 系统处理
|
|
||||||
if (hooks.has(nestedFieldPath)) {
|
if (hooks.has(nestedFieldPath)) {
|
||||||
const hookEntry = hooks.get(nestedFieldPath)
|
const hookEntry = hooks.get(nestedFieldPath)
|
||||||
if (!hookEntry) return null
|
if (!hookEntry) return null
|
||||||
@@ -192,49 +285,39 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
|||||||
? nestedSchema.classDoc
|
? nestedSchema.classDoc
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// 一级嵌套:使用 Card 包裹,清晰的 section 边界
|
|
||||||
if (level === 0) {
|
if (level === 0) {
|
||||||
return (
|
return (
|
||||||
<Card key={key}>
|
<DynamicConfigSection
|
||||||
<CardHeader className="pb-4">
|
key={key}
|
||||||
<div className="flex items-center gap-2">
|
nestedSchema={nestedSchema}
|
||||||
{renderSectionIcon(nestedSchema.uiIcon)}
|
values={(values[key] as Record<string, unknown>) || {}}
|
||||||
<CardTitle className="text-lg">{sectionTitle}</CardTitle>
|
onChange={(field, value) => onChange(`${key}.${field}`, value)}
|
||||||
</div>
|
basePath={nestedFieldPath}
|
||||||
{sectionDescription && (
|
hooks={hooks}
|
||||||
<CardDescription>{sectionDescription}</CardDescription>
|
level={level + 1}
|
||||||
)}
|
sectionTitle={sectionTitle}
|
||||||
</CardHeader>
|
sectionDescription={sectionDescription}
|
||||||
<CardContent>
|
/>
|
||||||
<DynamicConfigForm
|
|
||||||
schema={nestedSchema}
|
|
||||||
values={(values[key] as Record<string, unknown>) || {}}
|
|
||||||
onChange={(field, value) => onChange(`${key}.${field}`, value)}
|
|
||||||
basePath={nestedFieldPath}
|
|
||||||
hooks={hooks}
|
|
||||||
level={level + 1}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 二级及更深嵌套:使用左侧指示条 + 轻量分组
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="relative space-y-4 rounded-lg border-l-2 border-muted-foreground/20 pl-4 pt-1 pb-1"
|
className="relative space-y-4 rounded-lg border-l-2 border-muted-foreground/20 pl-4 pt-1 pb-1"
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-1">
|
||||||
{renderSectionIcon(nestedSchema.uiIcon)}
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="text-sm font-semibold">{sectionTitle}</h4>
|
<SectionIcon iconName={nestedSchema.uiIcon} />
|
||||||
|
<h4 className="text-sm font-semibold">{sectionTitle}</h4>
|
||||||
|
</div>
|
||||||
|
{sectionDescription && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{sectionDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{sectionDescription && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{sectionDescription}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DynamicConfigForm
|
<DynamicConfigForm
|
||||||
|
|||||||
@@ -313,20 +313,22 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Label with icon */}
|
<div className="space-y-0.5">
|
||||||
<Label className="text-sm font-medium flex items-center gap-2">
|
{/* Label with icon */}
|
||||||
{renderIcon()}
|
<Label className="text-sm font-medium flex items-center gap-2">
|
||||||
{schema.label}
|
{renderIcon()}
|
||||||
{schema.required && <span className="text-destructive">*</span>}
|
{schema.label}
|
||||||
</Label>
|
{schema.required && <span className="text-destructive">*</span>}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{schema.description && (
|
||||||
|
<p className="text-[13px] text-muted-foreground whitespace-pre-line">{schema.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Input component */}
|
{/* Input component */}
|
||||||
{renderInputComponent()}
|
{renderInputComponent()}
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
{schema.description && (
|
|
||||||
<p className="text-[13px] text-muted-foreground whitespace-pre-line">{schema.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, LayoutGrid, MessageSquare, Network, Package, Server, Settings, Sliders, Smile, UserCircle } from 'lucide-react'
|
import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, MessageSquare, Network, Package, Server, Settings, Sliders, Smile, UserCircle } from 'lucide-react'
|
||||||
|
|
||||||
import type { MenuSection } from './types'
|
import type { MenuSection } from './types'
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ export const menuSections: MenuSection[] = [
|
|||||||
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot', searchDescription: 'search.items.botConfigDesc' },
|
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot', searchDescription: 'search.items.botConfigDesc' },
|
||||||
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', searchDescription: 'search.items.modelProviderDesc', tourId: 'sidebar-model-provider' },
|
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', searchDescription: 'search.items.modelProviderDesc', tourId: 'sidebar-model-provider' },
|
||||||
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', searchDescription: 'search.items.modelDesc', tourId: 'sidebar-model-management' },
|
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', searchDescription: 'search.items.modelDesc', tourId: 'sidebar-model-management' },
|
||||||
{ icon: Sliders, label: 'sidebar.menu.adapterConfig', path: '/config/adapter' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,7 +32,6 @@ export const menuSections: MenuSection[] = [
|
|||||||
title: 'sidebar.groups.extensionsMonitor',
|
title: 'sidebar.groups.extensionsMonitor',
|
||||||
items: [
|
items: [
|
||||||
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins', searchDescription: 'search.items.pluginsDesc' },
|
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins', searchDescription: 'search.items.pluginsDesc' },
|
||||||
{ icon: LayoutGrid, label: 'sidebar.menu.configTemplate', path: '/config/pack-market' },
|
|
||||||
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
|
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
|
||||||
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
|
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
|
||||||
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
|
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
|
||||||
|
|||||||
@@ -500,17 +500,13 @@
|
|||||||
"title": "Personality",
|
"title": "Personality",
|
||||||
"description": "Define the bot's personality and speaking style"
|
"description": "Define the bot's personality and speaking style"
|
||||||
},
|
},
|
||||||
"emoji": {
|
"apiProvider": {
|
||||||
"title": "Emoji",
|
|
||||||
"description": "Configure emoji-related settings"
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"title": "Other Settings",
|
|
||||||
"description": "Configure global slang and other basic options"
|
|
||||||
},
|
|
||||||
"siliconFlow": {
|
|
||||||
"title": "API Setup",
|
"title": "API Setup",
|
||||||
"description": "Configure the SiliconFlow API key"
|
"description": "Configure the API provider"
|
||||||
|
},
|
||||||
|
"modelSetup": {
|
||||||
|
"title": "Model Setup",
|
||||||
|
"description": "Configure planner and replyer models"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
@@ -528,7 +524,12 @@
|
|||||||
"selectPlatform": "Please select a platform",
|
"selectPlatform": "Please select a platform",
|
||||||
"enterNickname": "Please enter a nickname",
|
"enterNickname": "Please enter a nickname",
|
||||||
"enterQqAccount": "Please enter a QQ account",
|
"enterQqAccount": "Please enter a QQ account",
|
||||||
"enterAccountId": "Please enter an account ID"
|
"enterAccountId": "Please enter an account ID",
|
||||||
|
"enterProviderName": "Please enter an API provider name",
|
||||||
|
"enterBaseUrl": "Please enter the API base URL",
|
||||||
|
"enterApiKey": "Please enter the API key",
|
||||||
|
"enterPlannerModelIdentifier": "Please enter the planner model identifier",
|
||||||
|
"enterReplyerModelIdentifier": "Please enter the replyer model identifier"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailedTitle": "Failed to load configuration",
|
"loadFailedTitle": "Failed to load configuration",
|
||||||
@@ -667,33 +668,43 @@
|
|||||||
"description": "Allow the bot to learn and use group-specific slang"
|
"description": "Allow the bot to learn and use group-specific slang"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"siliconFlow": {
|
"apiProvider": {
|
||||||
"about": {
|
"providerName": {
|
||||||
"title": "About SiliconFlow",
|
"label": "API Provider Name *",
|
||||||
"description": "SiliconFlow provides broad model coverage, including DeepSeek V3, Qwen, vision models, speech recognition, and embedding models. A single API key unlocks all MaiBot features.",
|
"placeholder": "For example OpenAI, DeepSeek, or self-hosted",
|
||||||
"link": "Get an API key from SiliconFlow"
|
"description": "This name is written to model_config.toml and referenced by the models below"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"label": "API Base URL *",
|
||||||
|
"description": "Enter an OpenAI-compatible endpoint, for example https://api.example.com/v1"
|
||||||
},
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"label": "SiliconFlow API Key *",
|
"label": "API Key *",
|
||||||
"description": "Enter your SiliconFlow API key. Once provided, MaiBot will automatically configure all required models.",
|
"description": "Enter the API key for this provider",
|
||||||
"show": "Show API key",
|
"show": "Show API key",
|
||||||
"hide": "Hide API key"
|
"hide": "Hide API key"
|
||||||
},
|
}
|
||||||
"autoConfig": {
|
},
|
||||||
"title": "The following models will be configured automatically:",
|
"modelSetup": {
|
||||||
"items": {
|
"planner": {
|
||||||
"deepseek": "DeepSeek V3 - primary chat and tool model",
|
"identifier": {
|
||||||
"qwen3": "Qwen3 30B - frequent small tasks and tool calls",
|
"label": "planner Model Identifier *",
|
||||||
"qwen3Vl": "Qwen3 VL 30B - image recognition",
|
"description": "The real model ID provided by the API service; the model name will be initialized from it"
|
||||||
"senseVoice": "SenseVoice - speech recognition",
|
},
|
||||||
"bgeM3": "BGE-M3 - text embeddings",
|
"visual": {
|
||||||
"lpmm": "Knowledge-base-related models (LPMM)"
|
"label": "Enable vision"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hint": {
|
"replyer": {
|
||||||
"title": "Tip: ",
|
"identifier": {
|
||||||
"description": "After finishing the wizard, you can add more API providers and models in \"System Settings -> Model Config\"."
|
"label": "replyer Model Identifier *",
|
||||||
}
|
"description": "The real model ID provided by the API service; the model name will be initialized from it"
|
||||||
|
},
|
||||||
|
"visual": {
|
||||||
|
"label": "Enable vision"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveHint": "You can configure more detailed task assignment later."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -500,17 +500,13 @@
|
|||||||
"title": "人格設定",
|
"title": "人格設定",
|
||||||
"description": "ボットの性格や話し方を定義します"
|
"description": "ボットの性格や話し方を定義します"
|
||||||
},
|
},
|
||||||
"emoji": {
|
"apiProvider": {
|
||||||
"title": "絵文字パック",
|
|
||||||
"description": "絵文字パック関連の設定を行います"
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"title": "その他の設定",
|
|
||||||
"description": "グローバルスラングなどの基本オプションを設定します"
|
|
||||||
},
|
|
||||||
"siliconFlow": {
|
|
||||||
"title": "API設定",
|
"title": "API設定",
|
||||||
"description": "SiliconFlow API キーを設定します"
|
"description": "APIプロバイダーを設定します"
|
||||||
|
},
|
||||||
|
"modelSetup": {
|
||||||
|
"title": "モデル設定",
|
||||||
|
"description": "planner と replyer モデルを設定します"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
@@ -528,7 +524,12 @@
|
|||||||
"selectPlatform": "プラットフォームを選択してください",
|
"selectPlatform": "プラットフォームを選択してください",
|
||||||
"enterNickname": "ニックネームを入力してください",
|
"enterNickname": "ニックネームを入力してください",
|
||||||
"enterQqAccount": "QQ アカウントを入力してください",
|
"enterQqAccount": "QQ アカウントを入力してください",
|
||||||
"enterAccountId": "アカウント ID を入力してください"
|
"enterAccountId": "アカウント ID を入力してください",
|
||||||
|
"enterProviderName": "APIプロバイダー名を入力してください",
|
||||||
|
"enterBaseUrl": "API Base URL を入力してください",
|
||||||
|
"enterApiKey": "API Key を入力してください",
|
||||||
|
"enterPlannerModelIdentifier": "planner モデル識別子を入力してください",
|
||||||
|
"enterReplyerModelIdentifier": "replyer モデル識別子を入力してください"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailedTitle": "設定の読み込みに失敗しました",
|
"loadFailedTitle": "設定の読み込みに失敗しました",
|
||||||
@@ -667,33 +668,43 @@
|
|||||||
"description": "グループ内のスラングを学習して使えるようにします"
|
"description": "グループ内のスラングを学習して使えるようにします"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"siliconFlow": {
|
"apiProvider": {
|
||||||
"about": {
|
"providerName": {
|
||||||
"title": "SiliconFlow について",
|
"label": "APIプロバイダー名 *",
|
||||||
"description": "SiliconFlow は DeepSeek V3、Qwen、ビジョンモデル、音声認識、埋め込みモデルなど幅広いモデルを提供します。API Key が1つあれば MaiBot の全機能を利用できます。",
|
"placeholder": "例: OpenAI、DeepSeek、自ホストサービス",
|
||||||
"link": "SiliconFlow で API Key を取得する"
|
"description": "この名前は model_config.toml に保存され、下のモデルから参照されます"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"label": "API Base URL *",
|
||||||
|
"description": "OpenAI互換エンドポイントを入力してください。例: https://api.example.com/v1"
|
||||||
},
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"label": "SiliconFlow API Key *",
|
"label": "API Key *",
|
||||||
"description": "SiliconFlow の API Key を入力してください。入力後、MaiBot が必要なモデルを自動設定します。",
|
"description": "このプロバイダーの API Key を入力してください",
|
||||||
"show": "API Key を表示",
|
"show": "API Key を表示",
|
||||||
"hide": "API Key を隠す"
|
"hide": "API Key を非表示"
|
||||||
},
|
}
|
||||||
"autoConfig": {
|
},
|
||||||
"title": "以下のモデルが自動設定されます:",
|
"modelSetup": {
|
||||||
"items": {
|
"planner": {
|
||||||
"deepseek": "DeepSeek V3 - メインの会話・ツールモデル",
|
"identifier": {
|
||||||
"qwen3": "Qwen3 30B - 頻繁な小タスクとツール呼び出し",
|
"label": "planner モデル識別子 *",
|
||||||
"qwen3Vl": "Qwen3 VL 30B - 画像認識",
|
"description": "APIサービスが提供する実際のモデルID。モデル名はこの識別子で初期化されます"
|
||||||
"senseVoice": "SenseVoice - 音声認識",
|
},
|
||||||
"bgeM3": "BGE-M3 - テキスト埋め込み",
|
"visual": {
|
||||||
"lpmm": "知識ベース関連モデル (LPMM)"
|
"label": "ビジョンを有効化"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hint": {
|
"replyer": {
|
||||||
"title": "ヒント:",
|
"identifier": {
|
||||||
"description": "ウィザード完了後は、「システム設定 -> モデル設定」でさらに API プロバイダーやモデルを追加できます。"
|
"label": "replyer モデル識別子 *",
|
||||||
}
|
"description": "APIサービスが提供する実際のモデルID。モデル名はこの識別子で初期化されます"
|
||||||
|
},
|
||||||
|
"visual": {
|
||||||
|
"label": "ビジョンを有効化"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveHint": "より詳細なタスク割り当ては後で設定できます。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -500,17 +500,13 @@
|
|||||||
"title": "성격 설정",
|
"title": "성격 설정",
|
||||||
"description": "봇의 성격과 말투를 정의합니다"
|
"description": "봇의 성격과 말투를 정의합니다"
|
||||||
},
|
},
|
||||||
"emoji": {
|
"apiProvider": {
|
||||||
"title": "이모지 팩",
|
|
||||||
"description": "이모지 관련 설정을 구성합니다"
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"title": "기타 설정",
|
|
||||||
"description": "전역 슬랭 등 기본 옵션을 설정합니다"
|
|
||||||
},
|
|
||||||
"siliconFlow": {
|
|
||||||
"title": "API 설정",
|
"title": "API 설정",
|
||||||
"description": "SiliconFlow API 키를 설정합니다"
|
"description": "API 제공자를 설정합니다"
|
||||||
|
},
|
||||||
|
"modelSetup": {
|
||||||
|
"title": "모델 설정",
|
||||||
|
"description": "planner와 replyer 모델을 설정합니다"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
@@ -528,7 +524,12 @@
|
|||||||
"selectPlatform": "플랫폼을 선택해 주세요",
|
"selectPlatform": "플랫폼을 선택해 주세요",
|
||||||
"enterNickname": "닉네임을 입력해 주세요",
|
"enterNickname": "닉네임을 입력해 주세요",
|
||||||
"enterQqAccount": "QQ 계정을 입력해 주세요",
|
"enterQqAccount": "QQ 계정을 입력해 주세요",
|
||||||
"enterAccountId": "계정 ID를 입력해 주세요"
|
"enterAccountId": "계정 ID를 입력해 주세요",
|
||||||
|
"enterProviderName": "API 제공자 이름을 입력해 주세요",
|
||||||
|
"enterBaseUrl": "API Base URL을 입력해 주세요",
|
||||||
|
"enterApiKey": "API Key를 입력해 주세요",
|
||||||
|
"enterPlannerModelIdentifier": "planner 모델 식별자를 입력해 주세요",
|
||||||
|
"enterReplyerModelIdentifier": "replyer 모델 식별자를 입력해 주세요"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailedTitle": "설정 불러오기에 실패했습니다",
|
"loadFailedTitle": "설정 불러오기에 실패했습니다",
|
||||||
@@ -667,33 +668,43 @@
|
|||||||
"description": "봇이 그룹 슬랭을 학습하고 사용할 수 있게 합니다"
|
"description": "봇이 그룹 슬랭을 학습하고 사용할 수 있게 합니다"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"siliconFlow": {
|
"apiProvider": {
|
||||||
"about": {
|
"providerName": {
|
||||||
"title": "SiliconFlow 소개",
|
"label": "API 제공자 이름 *",
|
||||||
"description": "SiliconFlow 는 DeepSeek V3, Qwen, 비전 모델, 음성 인식, 임베딩 모델 등 폭넓은 모델을 제공합니다. API Key 하나로 MaiBot 의 모든 기능을 사용할 수 있습니다.",
|
"placeholder": "예: OpenAI, DeepSeek, 자체 호스팅",
|
||||||
"link": "SiliconFlow 에서 API Key 받기"
|
"description": "이 이름은 model_config.toml에 저장되며 아래 모델에서 참조됩니다"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"label": "API Base URL *",
|
||||||
|
"description": "OpenAI 호환 엔드포인트를 입력해 주세요. 예: https://api.example.com/v1"
|
||||||
},
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"label": "SiliconFlow API Key *",
|
"label": "API Key *",
|
||||||
"description": "SiliconFlow API Key를 입력해 주세요. 입력하면 MaiBot 이 필요한 모델을 자동으로 구성합니다.",
|
"description": "이 제공자의 API Key를 입력해 주세요",
|
||||||
"show": "API Key 표시",
|
"show": "API Key 표시",
|
||||||
"hide": "API Key 숨기기"
|
"hide": "API Key 숨기기"
|
||||||
},
|
}
|
||||||
"autoConfig": {
|
},
|
||||||
"title": "다음 모델이 자동으로 구성됩니다:",
|
"modelSetup": {
|
||||||
"items": {
|
"planner": {
|
||||||
"deepseek": "DeepSeek V3 - 주요 대화 및 도구 모델",
|
"identifier": {
|
||||||
"qwen3": "Qwen3 30B - 잦은 소규모 작업과 도구 호출",
|
"label": "planner 모델 식별자 *",
|
||||||
"qwen3Vl": "Qwen3 VL 30B - 이미지 인식",
|
"description": "API 서비스가 제공하는 실제 모델 ID입니다. 모델 이름은 이 식별자로 초기화됩니다"
|
||||||
"senseVoice": "SenseVoice - 음성 인식",
|
},
|
||||||
"bgeM3": "BGE-M3 - 텍스트 임베딩",
|
"visual": {
|
||||||
"lpmm": "지식 베이스 관련 모델 (LPMM)"
|
"label": "비전 사용"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hint": {
|
"replyer": {
|
||||||
"title": "팁: ",
|
"identifier": {
|
||||||
"description": "마법사를 마친 뒤에는 \"시스템 설정 -> 모델 설정\"에서 더 많은 API 제공자와 모델을 추가할 수 있습니다."
|
"label": "replyer 모델 식별자 *",
|
||||||
}
|
"description": "API 서비스가 제공하는 실제 모델 ID입니다. 모델 이름은 이 식별자로 초기화됩니다"
|
||||||
|
},
|
||||||
|
"visual": {
|
||||||
|
"label": "비전 사용"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveHint": "더 자세한 작업 할당은 나중에 설정할 수 있습니다."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -500,17 +500,13 @@
|
|||||||
"title": "人格配置",
|
"title": "人格配置",
|
||||||
"description": "定义机器人的性格和说话风格"
|
"description": "定义机器人的性格和说话风格"
|
||||||
},
|
},
|
||||||
"emoji": {
|
"apiProvider": {
|
||||||
"title": "表情包",
|
|
||||||
"description": "配置表情包相关设置"
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"title": "其他设置",
|
|
||||||
"description": "配置全局黑话等基础选项"
|
|
||||||
},
|
|
||||||
"siliconFlow": {
|
|
||||||
"title": "API配置",
|
"title": "API配置",
|
||||||
"description": "配置硅基流动 API 密钥"
|
"description": "配置 API 提供商"
|
||||||
|
},
|
||||||
|
"modelSetup": {
|
||||||
|
"title": "模型配置",
|
||||||
|
"description": "配置 planner 和 replyer 模型"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
@@ -528,7 +524,12 @@
|
|||||||
"selectPlatform": "请选择平台",
|
"selectPlatform": "请选择平台",
|
||||||
"enterNickname": "请输入昵称",
|
"enterNickname": "请输入昵称",
|
||||||
"enterQqAccount": "请输入 QQ 账号",
|
"enterQqAccount": "请输入 QQ 账号",
|
||||||
"enterAccountId": "请输入账号 ID"
|
"enterAccountId": "请输入账号 ID",
|
||||||
|
"enterProviderName": "请输入 API 提供商名称",
|
||||||
|
"enterBaseUrl": "请输入 API Base URL",
|
||||||
|
"enterApiKey": "请输入 API Key",
|
||||||
|
"enterPlannerModelIdentifier": "请输入 planner 模型标识符",
|
||||||
|
"enterReplyerModelIdentifier": "请输入 replyer 模型标识符"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailedTitle": "加载配置失败",
|
"loadFailedTitle": "加载配置失败",
|
||||||
@@ -559,8 +560,8 @@
|
|||||||
"footer": "您可以随时在设置中修改这些配置",
|
"footer": "您可以随时在设置中修改这些配置",
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"personality": {
|
"personality": {
|
||||||
"personality": "是一个女大学生,现在在读大二,会刷贴吧。",
|
"personality": "是一个大二女大学生,现在正在上网和群友聊天。有轻微攻击性",
|
||||||
"replyStyle": "请回复得平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧、知乎和微博的回复风格。",
|
"replyStyle": "你的风格平淡简短。可以参考贴吧,知乎和微博的回复风格。不浮夸不长篇大论,不要过分修辞和复杂句。",
|
||||||
"multipleReplyStyles": {
|
"multipleReplyStyles": {
|
||||||
"plain": "你的风格平淡但不失讽刺,很简短,很白话。可以参考贴吧、微博的回复风格。",
|
"plain": "你的风格平淡但不失讽刺,很简短,很白话。可以参考贴吧、微博的回复风格。",
|
||||||
"shortText": "用1-2个字进行回复",
|
"shortText": "用1-2个字进行回复",
|
||||||
@@ -577,7 +578,7 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"label": "平台 *",
|
"label": "平台 *",
|
||||||
"placeholder": "请选择平台",
|
"placeholder": "请选择平台",
|
||||||
"description": "选择机器人运行的平台",
|
"description": "选择麦麦Bot运行的平台",
|
||||||
"options": {
|
"options": {
|
||||||
"custom": "其他平台"
|
"custom": "其他平台"
|
||||||
}
|
}
|
||||||
@@ -589,7 +590,7 @@
|
|||||||
"qqAccount": {
|
"qqAccount": {
|
||||||
"label": "QQ账号 *",
|
"label": "QQ账号 *",
|
||||||
"placeholder": "请输入机器人的 QQ 账号",
|
"placeholder": "请输入机器人的 QQ 账号",
|
||||||
"description": "机器人登录使用的 QQ 账号"
|
"description": "运行麦麦Bot的 QQ 账号"
|
||||||
},
|
},
|
||||||
"primaryAccount": {
|
"primaryAccount": {
|
||||||
"label": "账号 ID *",
|
"label": "账号 ID *",
|
||||||
@@ -599,7 +600,7 @@
|
|||||||
"nickname": {
|
"nickname": {
|
||||||
"label": "昵称 *",
|
"label": "昵称 *",
|
||||||
"placeholder": "请输入机器人的昵称",
|
"placeholder": "请输入机器人的昵称",
|
||||||
"description": "机器人的主要称呼名称"
|
"description": "麦麦Bot的名称"
|
||||||
},
|
},
|
||||||
"alias": {
|
"alias": {
|
||||||
"label": "别名",
|
"label": "别名",
|
||||||
@@ -667,33 +668,43 @@
|
|||||||
"description": "允许机器人学习和使用群组黑话"
|
"description": "允许机器人学习和使用群组黑话"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"siliconFlow": {
|
"apiProvider": {
|
||||||
"about": {
|
"providerName": {
|
||||||
"title": "关于硅基流动 (SiliconFlow)",
|
"label": "API 提供商名称 *",
|
||||||
"description": "硅基流动提供了完整的模型覆盖,包括 DeepSeek V3、Qwen、视觉模型、语音识别和嵌入模型。只需一个 API Key 即可使用麦麦的所有功能!",
|
"placeholder": "例如 OpenAI、DeepSeek、自建服务",
|
||||||
"link": "前往硅基流动获取 API Key"
|
"description": "为api提供商命名"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"label": "API Base URL *",
|
||||||
|
"description": "请填写 OpenAI 兼容接口地址,例如 https://api.example.com/v1"
|
||||||
},
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"label": "SiliconFlow API Key *",
|
"label": "API Key *",
|
||||||
"description": "请输入您的硅基流动 API 密钥。获取后,麦麦将自动配置所有必需的模型。",
|
"description": "请填写该提供商的 API Key",
|
||||||
"show": "显示 API Key",
|
"show": "显示 API Key",
|
||||||
"hide": "隐藏 API Key"
|
"hide": "隐藏 API Key"
|
||||||
},
|
}
|
||||||
"autoConfig": {
|
},
|
||||||
"title": "将自动配置以下模型:",
|
"modelSetup": {
|
||||||
"items": {
|
"planner": {
|
||||||
"deepseek": "DeepSeek V3 - 主要对话和工具模型",
|
"identifier": {
|
||||||
"qwen3": "Qwen3 30B - 高频小任务和工具调用",
|
"label": "planner 模型标识符 *",
|
||||||
"qwen3Vl": "Qwen3 VL 30B - 图像识别",
|
"description": "API 服务商提供的真实模型 ID,模型名称会自动初始化为该标识符"
|
||||||
"senseVoice": "SenseVoice - 语音识别",
|
},
|
||||||
"bgeM3": "BGE-M3 - 文本嵌入",
|
"visual": {
|
||||||
"lpmm": "知识库相关模型 (LPMM)"
|
"label": "启用视觉"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hint": {
|
"replyer": {
|
||||||
"title": "💡 提示:",
|
"identifier": {
|
||||||
"description": "完成向导后,您可以在“系统设置 -> 模型配置”中添加更多 API 提供商和模型。"
|
"label": "replyer 模型标识符 *",
|
||||||
}
|
"description": "API 服务商提供的真实模型 ID,模型名称会自动初始化为该标识符"
|
||||||
|
},
|
||||||
|
"visual": {
|
||||||
|
"label": "启用视觉"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saveHint": "你可以稍后配置更详细的任务分配。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export interface MaisakaToolCall {
|
|||||||
export interface SessionStartEvent {
|
export interface SessionStartEvent {
|
||||||
session_id: string
|
session_id: string
|
||||||
session_name: string
|
session_name: string
|
||||||
|
is_group_chat?: boolean
|
||||||
|
group_id?: string | null
|
||||||
|
user_id?: string | null
|
||||||
|
platform?: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ interface PluginApiResponse {
|
|||||||
}
|
}
|
||||||
homepage_url?: string
|
homepage_url?: string
|
||||||
repository_url?: string
|
repository_url?: string
|
||||||
|
urls?: {
|
||||||
|
repository?: string
|
||||||
|
homepage?: string
|
||||||
|
documentation?: string
|
||||||
|
issues?: string
|
||||||
|
}
|
||||||
keywords: string[]
|
keywords: string[]
|
||||||
categories?: string[]
|
categories?: string[]
|
||||||
default_locale: string
|
default_locale: string
|
||||||
@@ -44,6 +50,28 @@ interface PluginApiResponse {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePluginManifest(manifest: PluginApiResponse['manifest']): PluginInfo['manifest'] {
|
||||||
|
const repositoryUrl = manifest.repository_url || manifest.urls?.repository
|
||||||
|
const homepageUrl = manifest.homepage_url || manifest.urls?.homepage
|
||||||
|
|
||||||
|
return {
|
||||||
|
manifest_version: manifest.manifest_version || 1,
|
||||||
|
name: manifest.name,
|
||||||
|
version: manifest.version,
|
||||||
|
description: manifest.description || '',
|
||||||
|
author: manifest.author || { name: 'Unknown' },
|
||||||
|
license: manifest.license || 'Unknown',
|
||||||
|
host_application: manifest.host_application || { min_version: '0.0.0' },
|
||||||
|
homepage_url: homepageUrl,
|
||||||
|
repository_url: repositoryUrl,
|
||||||
|
urls: manifest.urls,
|
||||||
|
keywords: manifest.keywords || [],
|
||||||
|
categories: manifest.categories || [],
|
||||||
|
default_locale: manifest.default_locale || 'zh-CN',
|
||||||
|
locales_path: manifest.locales_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从远程获取插件列表(通过后端代理避免 CORS)
|
* 从远程获取插件列表(通过后端代理避免 CORS)
|
||||||
*/
|
*/
|
||||||
@@ -88,21 +116,7 @@ export async function fetchPluginList(): Promise<ApiResponse<PluginInfo[]>> {
|
|||||||
})
|
})
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
manifest: {
|
manifest: normalizePluginManifest(item.manifest),
|
||||||
manifest_version: item.manifest.manifest_version || 1,
|
|
||||||
name: item.manifest.name,
|
|
||||||
version: item.manifest.version,
|
|
||||||
description: item.manifest.description || '',
|
|
||||||
author: item.manifest.author || { name: 'Unknown' },
|
|
||||||
license: item.manifest.license || 'Unknown',
|
|
||||||
host_application: item.manifest.host_application || { min_version: '0.0.0' },
|
|
||||||
homepage_url: item.manifest.homepage_url,
|
|
||||||
repository_url: item.manifest.repository_url,
|
|
||||||
keywords: item.manifest.keywords || [],
|
|
||||||
categories: item.manifest.categories || [],
|
|
||||||
default_locale: item.manifest.default_locale || 'zh-CN',
|
|
||||||
locales_path: item.manifest.locales_path,
|
|
||||||
},
|
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
rating: 0,
|
rating: 0,
|
||||||
review_count: 0,
|
review_count: 0,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
BarChart3,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
|
ExternalLink,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -566,6 +568,13 @@ function IndexPageContent() {
|
|||||||
{t('home.quickActions.systemSettings')}
|
{t('home.quickActions.systemSettings')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" asChild className="gap-2">
|
||||||
|
<a href="/maibot_statistics.html" target="_blank" rel="noopener noreferrer">
|
||||||
|
<BarChart3 className="h-4 w-4" />
|
||||||
|
详细统计数据
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -104,10 +104,17 @@ function SessionSidebar({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span className="font-medium truncate max-w-35">
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
{session.sessionName}
|
{session.isGroupChat !== undefined && (
|
||||||
</span>
|
<Badge variant="outline" className="h-4 shrink-0 px-1 text-[10px]">
|
||||||
<Badge variant="secondary" className="text-[10px] h-4 px-1">
|
{session.isGroupChat ? '群' : '私'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<span className="truncate font-medium" title={session.sessionName}>
|
||||||
|
{session.sessionName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="h-4 shrink-0 px-1 text-[10px]">
|
||||||
{session.eventCount}
|
{session.eventCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export interface TimelineEntry {
|
|||||||
export interface SessionInfo {
|
export interface SessionInfo {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
sessionName: string
|
sessionName: string
|
||||||
|
isGroupChat?: boolean
|
||||||
|
groupId?: string | null
|
||||||
|
userId?: string | null
|
||||||
|
platform?: string
|
||||||
lastActivity: number
|
lastActivity: number
|
||||||
eventCount: number
|
eventCount: number
|
||||||
}
|
}
|
||||||
@@ -33,18 +37,62 @@ export interface SessionInfo {
|
|||||||
/** 最大保留的时间线条目数 */
|
/** 最大保留的时间线条目数 */
|
||||||
const MAX_TIMELINE_ENTRIES = 500
|
const MAX_TIMELINE_ENTRIES = 500
|
||||||
|
|
||||||
|
function resolveSessionDisplayName({
|
||||||
|
fallbackName,
|
||||||
|
groupId,
|
||||||
|
isGroupChat,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
fallbackName?: string
|
||||||
|
groupId?: string | null
|
||||||
|
isGroupChat?: boolean
|
||||||
|
sessionId: string
|
||||||
|
userId?: string | null
|
||||||
|
}) {
|
||||||
|
const targetId = isGroupChat ? groupId : userId
|
||||||
|
const normalizedName = fallbackName?.trim()
|
||||||
|
|
||||||
|
if (targetId && normalizedName?.endsWith(`(${targetId})`)) {
|
||||||
|
return normalizedName
|
||||||
|
}
|
||||||
|
if (normalizedName && targetId && normalizedName !== targetId && normalizedName !== sessionId) {
|
||||||
|
return `${normalizedName}(${targetId})`
|
||||||
|
}
|
||||||
|
if (isGroupChat && groupId) {
|
||||||
|
return groupId
|
||||||
|
}
|
||||||
|
if (!isGroupChat && userId) {
|
||||||
|
return userId
|
||||||
|
}
|
||||||
|
return fallbackName || sessionId.slice(0, 8)
|
||||||
|
}
|
||||||
|
|
||||||
let entryCounter = 0
|
let entryCounter = 0
|
||||||
|
let cachedTimeline: TimelineEntry[] = []
|
||||||
|
let cachedSessions: Map<string, SessionInfo> = new Map()
|
||||||
|
let cachedSelectedSession: string | null = null
|
||||||
|
|
||||||
export function useMaisakaMonitor() {
|
export function useMaisakaMonitor() {
|
||||||
const [timeline, setTimeline] = useState<TimelineEntry[]>([])
|
const [timeline, setTimeline] = useState<TimelineEntry[]>(cachedTimeline)
|
||||||
const [sessions, setSessions] = useState<Map<string, SessionInfo>>(new Map())
|
const [sessions, setSessions] = useState<Map<string, SessionInfo>>(new Map(cachedSessions))
|
||||||
const [selectedSession, setSelectedSession] = useState<string | null>(null)
|
const [selectedSession, setSelectedSessionState] = useState<string | null>(cachedSelectedSession)
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
const unsubRef = useRef<(() => Promise<void>) | null>(null)
|
const unsubRef = useRef<(() => Promise<void>) | null>(null)
|
||||||
|
|
||||||
const handleEvent = useCallback((event: MaisakaMonitorEvent) => {
|
const handleEvent = useCallback((event: MaisakaMonitorEvent) => {
|
||||||
const sessionId = (event.data as unknown as Record<string, unknown>).session_id as string
|
const dataRecord = event.data as unknown as Record<string, unknown>
|
||||||
const timestamp = (event.data as unknown as Record<string, unknown>).timestamp as number
|
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 = {
|
const entry: TimelineEntry = {
|
||||||
id: `evt_${++entryCounter}_${Date.now()}`,
|
id: `evt_${++entryCounter}_${Date.now()}`,
|
||||||
@@ -56,22 +104,34 @@ export function useMaisakaMonitor() {
|
|||||||
|
|
||||||
setTimeline((prev) => {
|
setTimeline((prev) => {
|
||||||
const next = [...prev, entry]
|
const next = [...prev, entry]
|
||||||
return next.length > MAX_TIMELINE_ENTRIES
|
const trimmed = next.length > MAX_TIMELINE_ENTRIES
|
||||||
? next.slice(next.length - MAX_TIMELINE_ENTRIES)
|
? next.slice(next.length - MAX_TIMELINE_ENTRIES)
|
||||||
: next
|
: next
|
||||||
|
cachedTimeline = trimmed
|
||||||
|
return trimmed
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新会话信息
|
// 更新会话信息
|
||||||
if (event.type === 'session.start') {
|
if (event.type === 'session.start') {
|
||||||
const d = event.data
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
next.set(sessionId, {
|
next.set(sessionId, {
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionName: d.session_name,
|
sessionName: resolveSessionDisplayName({
|
||||||
|
fallbackName: sessionName,
|
||||||
|
groupId,
|
||||||
|
isGroupChat,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
isGroupChat,
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
platform,
|
||||||
lastActivity: timestamp,
|
lastActivity: timestamp,
|
||||||
eventCount: (prev.get(sessionId)?.eventCount ?? 0) + 1,
|
eventCount: (prev.get(sessionId)?.eventCount ?? 0) + 1,
|
||||||
})
|
})
|
||||||
|
cachedSessions = next
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -81,24 +141,51 @@ export function useMaisakaMonitor() {
|
|||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
next.set(sessionId, {
|
next.set(sessionId, {
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionName: sessionId.slice(0, 8),
|
sessionName: resolveSessionDisplayName({
|
||||||
|
fallbackName: sessionName,
|
||||||
|
groupId,
|
||||||
|
isGroupChat,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
isGroupChat,
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
platform,
|
||||||
lastActivity: timestamp,
|
lastActivity: timestamp,
|
||||||
eventCount: 1,
|
eventCount: 1,
|
||||||
})
|
})
|
||||||
|
cachedSessions = next
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
next.set(sessionId, {
|
next.set(sessionId, {
|
||||||
...existing,
|
...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,
|
lastActivity: timestamp,
|
||||||
eventCount: existing.eventCount + 1,
|
eventCount: existing.eventCount + 1,
|
||||||
})
|
})
|
||||||
|
cachedSessions = next
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动选中第一个会话
|
// 自动选中第一个会话
|
||||||
setSelectedSession((current) => current ?? sessionId)
|
setSelectedSessionState((current) => {
|
||||||
|
const next = current ?? sessionId
|
||||||
|
cachedSelectedSession = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -124,9 +211,15 @@ export function useMaisakaMonitor() {
|
|||||||
}, [handleEvent])
|
}, [handleEvent])
|
||||||
|
|
||||||
const clearTimeline = useCallback(() => {
|
const clearTimeline = useCallback(() => {
|
||||||
|
cachedTimeline = []
|
||||||
setTimeline([])
|
setTimeline([])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const setSelectedSession = useCallback((sessionId: string | null) => {
|
||||||
|
cachedSelectedSession = sessionId
|
||||||
|
setSelectedSessionState(sessionId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
/** 当前选中会话的时间线 */
|
/** 当前选中会话的时间线 */
|
||||||
const filteredTimeline = selectedSession
|
const filteredTimeline = selectedSession
|
||||||
? timeline.filter((e) => e.sessionId === selectedSession)
|
? timeline.filter((e) => e.sessionId === selectedSession)
|
||||||
|
|||||||
@@ -110,10 +110,20 @@ export function PluginDetailPage() {
|
|||||||
throw new Error('未找到该插件')
|
throw new Error('未找到该插件')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawManifest = foundPlugin.manifest || {}
|
||||||
|
const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository
|
||||||
|
const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage
|
||||||
|
|
||||||
// 转换为 PluginInfo 格式
|
// 转换为 PluginInfo 格式
|
||||||
const pluginInfo: PluginInfo = {
|
const pluginInfo: PluginInfo = {
|
||||||
id: foundPlugin.id,
|
id: foundPlugin.id,
|
||||||
manifest: foundPlugin.manifest,
|
manifest: {
|
||||||
|
...rawManifest,
|
||||||
|
homepage_url: homepageUrl,
|
||||||
|
repository_url: repositoryUrl,
|
||||||
|
default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN',
|
||||||
|
locales_path: rawManifest.locales_path || rawManifest.i18n?.locales_path,
|
||||||
|
},
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
rating: 0,
|
rating: 0,
|
||||||
review_count: 0,
|
review_count: 0,
|
||||||
@@ -270,7 +280,8 @@ export function PluginDetailPage() {
|
|||||||
try {
|
try {
|
||||||
setOperating(true)
|
setOperating(true)
|
||||||
|
|
||||||
const installResult = await installPlugin(plugin.id, plugin.manifest.repository_url || '', 'main')
|
const repositoryUrl = plugin.manifest.repository_url || plugin.manifest.urls?.repository || ''
|
||||||
|
const installResult = await installPlugin(plugin.id, repositoryUrl, 'main')
|
||||||
|
|
||||||
if (!installResult.success) {
|
if (!installResult.success) {
|
||||||
toast({
|
toast({
|
||||||
@@ -367,7 +378,8 @@ export function PluginDetailPage() {
|
|||||||
try {
|
try {
|
||||||
setOperating(true)
|
setOperating(true)
|
||||||
|
|
||||||
const updateResult = await updatePlugin(plugin.id, plugin.manifest.repository_url || '', 'main')
|
const repositoryUrl = plugin.manifest.repository_url || plugin.manifest.urls?.repository || ''
|
||||||
|
const updateResult = await updatePlugin(plugin.id, repositoryUrl, 'main')
|
||||||
|
|
||||||
if (!updateResult.success) {
|
if (!updateResult.success) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ function PluginsPageContent() {
|
|||||||
for (const installedPlugin of installed) {
|
for (const installedPlugin of installed) {
|
||||||
const existsInMarket = mergedData.some(p => p.id === installedPlugin.id)
|
const existsInMarket = mergedData.some(p => p.id === installedPlugin.id)
|
||||||
if (!existsInMarket && installedPlugin.manifest) {
|
if (!existsInMarket && installedPlugin.manifest) {
|
||||||
|
const urls = installedPlugin.manifest.urls as PluginInfo['manifest']['urls'] | undefined
|
||||||
// 添加本地插件到列表
|
// 添加本地插件到列表
|
||||||
mergedData.push({
|
mergedData.push({
|
||||||
id: installedPlugin.id,
|
id: installedPlugin.id,
|
||||||
@@ -225,8 +226,9 @@ function PluginsPageContent() {
|
|||||||
author: installedPlugin.manifest.author,
|
author: installedPlugin.manifest.author,
|
||||||
license: installedPlugin.manifest.license || 'Unknown',
|
license: installedPlugin.manifest.license || 'Unknown',
|
||||||
host_application: installedPlugin.manifest.host_application,
|
host_application: installedPlugin.manifest.host_application,
|
||||||
homepage_url: installedPlugin.manifest.homepage_url,
|
homepage_url: installedPlugin.manifest.homepage_url || urls?.homepage,
|
||||||
repository_url: installedPlugin.manifest.repository_url,
|
repository_url: installedPlugin.manifest.repository_url || urls?.repository,
|
||||||
|
urls,
|
||||||
keywords: installedPlugin.manifest.keywords || [],
|
keywords: installedPlugin.manifest.keywords || [],
|
||||||
categories: installedPlugin.manifest.categories || [],
|
categories: installedPlugin.manifest.categories || [],
|
||||||
default_locale: (installedPlugin.manifest.default_locale as string) || 'zh-CN',
|
default_locale: (installedPlugin.manifest.default_locale as string) || 'zh-CN',
|
||||||
@@ -430,7 +432,7 @@ function PluginsPageContent() {
|
|||||||
|
|
||||||
const installResult = await installPlugin(
|
const installResult = await installPlugin(
|
||||||
installingPlugin.id,
|
installingPlugin.id,
|
||||||
installingPlugin.manifest.repository_url || '',
|
installingPlugin.manifest.repository_url || installingPlugin.manifest.urls?.repository || '',
|
||||||
branch
|
branch
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -574,7 +576,7 @@ function PluginsPageContent() {
|
|||||||
try {
|
try {
|
||||||
const updateResult = await updatePlugin(
|
const updateResult = await updatePlugin(
|
||||||
plugin.id,
|
plugin.id,
|
||||||
plugin.manifest.repository_url || '',
|
plugin.manifest.repository_url || plugin.manifest.urls?.repository || '',
|
||||||
'main'
|
'main'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// 设置向导各步骤表单组件
|
// 设置向导各步骤表单组件
|
||||||
|
|
||||||
import { ExternalLink, Eye, EyeOff, X } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
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'
|
||||||
@@ -15,16 +14,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiProviderSetupConfig,
|
||||||
BotBasicConfig,
|
BotBasicConfig,
|
||||||
EmojiConfig,
|
ModelSetupConfig,
|
||||||
OtherBasicConfig,
|
|
||||||
PersonalityConfig,
|
PersonalityConfig,
|
||||||
SiliconFlowConfig,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
// ====== 步骤1:Bot基础配置 ======
|
// ====== 步骤1:Bot基础配置 ======
|
||||||
@@ -156,22 +153,6 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddAlias = (alias: string) => {
|
|
||||||
if (alias.trim() && !config.alias_names.includes(alias.trim())) {
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
alias_names: [...config.alias_names, alias.trim()],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveAlias = (index: number) => {
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
alias_names: config.alias_names.filter((_, aliasIndex) => aliasIndex !== index),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -254,53 +235,6 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
|||||||
{t('setupPage.forms.botBasic.nickname.description')}
|
{t('setupPage.forms.botBasic.nickname.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label>{t('setupPage.forms.botBasic.alias.label')}</Label>
|
|
||||||
<div className="mb-2 flex flex-wrap gap-2">
|
|
||||||
{config.alias_names.map((alias, index) => (
|
|
||||||
<Badge key={index} variant="secondary" className="gap-1">
|
|
||||||
{alias}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveAlias(index)}
|
|
||||||
className="hover:text-destructive ml-1"
|
|
||||||
aria-label={t('setupPage.forms.botBasic.alias.remove', { alias })}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="alias_input"
|
|
||||||
placeholder={t('setupPage.forms.botBasic.alias.placeholder')}
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleAddAlias((e.target as HTMLInputElement).value)
|
|
||||||
;(e.target as HTMLInputElement).value = ''
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const input = document.getElementById('alias_input') as HTMLInputElement
|
|
||||||
if (input) {
|
|
||||||
handleAddAlias(input.value)
|
|
||||||
input.value = ''
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('setupPage.forms.botBasic.alias.add')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.botBasic.alias.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -313,7 +247,6 @@ interface PersonalityFormProps {
|
|||||||
|
|
||||||
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
|
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const multipleReplyStyleText = config.multiple_reply_style.join('\n')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -344,276 +277,61 @@ export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
|
|||||||
{t('setupPage.forms.personality.replyStyle.description')}
|
{t('setupPage.forms.personality.replyStyle.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="multiple_reply_style">
|
|
||||||
{t('setupPage.forms.personality.multipleReplyStyle.label')}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="multiple_reply_style"
|
|
||||||
placeholder={t('setupPage.forms.personality.multipleReplyStyle.placeholder')}
|
|
||||||
value={multipleReplyStyleText}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
multiple_reply_style: e.target.value
|
|
||||||
.split('\n')
|
|
||||||
.map((style) => style.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
rows={5}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.personality.multipleReplyStyle.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="multiple_probability">
|
|
||||||
{t('setupPage.forms.personality.multipleProbability.label')}
|
|
||||||
</Label>
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
{(config.multiple_probability * 100).toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
id="multiple_probability"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value={config.multiple_probability}
|
|
||||||
onChange={(e) => onChange({ ...config, multiple_probability: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.personality.multipleProbability.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 步骤3:表情包配置 ======
|
// ====== 步骤3:API 提供商配置 ======
|
||||||
interface EmojiFormProps {
|
interface ApiProviderSetupFormProps {
|
||||||
config: EmojiConfig
|
config: ApiProviderSetupConfig
|
||||||
onChange: (config: EmojiConfig) => void
|
onChange: (config: ApiProviderSetupConfig) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
export function ApiProviderSetupForm({ config, onChange }: ApiProviderSetupFormProps) {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="emoji_send_num">{t('setupPage.forms.emoji.emojiSendNum.label')}</Label>
|
|
||||||
<Input
|
|
||||||
id="emoji_send_num"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="64"
|
|
||||||
value={config.emoji_send_num}
|
|
||||||
onChange={(e) => onChange({ ...config, emoji_send_num: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.emojiSendNum.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="max_reg_num">{t('setupPage.forms.emoji.maxRegNum.label')}</Label>
|
|
||||||
<Input
|
|
||||||
id="max_reg_num"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="200"
|
|
||||||
value={config.max_reg_num}
|
|
||||||
onChange={(e) => onChange({ ...config, max_reg_num: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.maxRegNum.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="do_replace">{t('setupPage.forms.emoji.doReplace.label')}</Label>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.doReplace.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="do_replace"
|
|
||||||
checked={config.do_replace}
|
|
||||||
onCheckedChange={(checked) => onChange({ ...config, do_replace: checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="check_interval">{t('setupPage.forms.emoji.checkInterval.label')}</Label>
|
|
||||||
<Input
|
|
||||||
id="check_interval"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="120"
|
|
||||||
value={config.check_interval}
|
|
||||||
onChange={(e) => onChange({ ...config, check_interval: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.checkInterval.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="steal_emoji">{t('setupPage.forms.emoji.stealEmoji.label')}</Label>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.stealEmoji.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="steal_emoji"
|
|
||||||
checked={config.steal_emoji}
|
|
||||||
onCheckedChange={(checked) => onChange({ ...config, steal_emoji: checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="content_filtration">
|
|
||||||
{t('setupPage.forms.emoji.contentFiltration.label')}
|
|
||||||
</Label>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.contentFiltration.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="content_filtration"
|
|
||||||
checked={config.content_filtration}
|
|
||||||
onCheckedChange={(checked) => onChange({ ...config, content_filtration: checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config.content_filtration && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="filtration_prompt">
|
|
||||||
{t('setupPage.forms.emoji.filtrationPrompt.label')}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="filtration_prompt"
|
|
||||||
placeholder={t('setupPage.forms.emoji.filtrationPrompt.placeholder')}
|
|
||||||
value={config.filtration_prompt}
|
|
||||||
onChange={(e) => onChange({ ...config, filtration_prompt: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.emoji.filtrationPrompt.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== 步骤4:其他基础配置 ======
|
|
||||||
interface OtherBasicFormProps {
|
|
||||||
config: OtherBasicConfig
|
|
||||||
onChange: (config: OtherBasicConfig) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="all_global">{t('setupPage.forms.other.allGlobal.label')}</Label>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{t('setupPage.forms.other.allGlobal.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="all_global"
|
|
||||||
checked={config.all_global}
|
|
||||||
onCheckedChange={(checked) => onChange({ ...config, all_global: checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== 步骤5:硅基流动API配置 ======
|
|
||||||
interface SiliconFlowFormProps {
|
|
||||||
config: SiliconFlowConfig
|
|
||||||
onChange: (config: SiliconFlowConfig) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showApiKey, setShowApiKey] = useState(false)
|
const [showApiKey, setShowApiKey] = useState(false)
|
||||||
const apiKeyToggleLabel = showApiKey
|
const apiKeyToggleLabel = showApiKey
|
||||||
? t('setupPage.forms.siliconFlow.apiKey.hide')
|
? t('setupPage.forms.apiProvider.apiKey.hide')
|
||||||
: t('setupPage.forms.siliconFlow.apiKey.show')
|
: t('setupPage.forms.apiProvider.apiKey.show')
|
||||||
const autoConfigItems = [
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.deepseek'),
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3'),
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3Vl'),
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.senseVoice'),
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.bgeM3'),
|
|
||||||
t('setupPage.forms.siliconFlow.autoConfig.items.lpmm'),
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-3">
|
<Label htmlFor="provider_name">{t('setupPage.forms.apiProvider.providerName.label')}</Label>
|
||||||
<div className="mt-0.5">
|
<Input
|
||||||
<svg
|
id="provider_name"
|
||||||
className="h-5 w-5 text-blue-600 dark:text-blue-400"
|
placeholder={t('setupPage.forms.apiProvider.providerName.placeholder')}
|
||||||
fill="none"
|
value={config.provider_name}
|
||||||
viewBox="0 0 24 24"
|
onChange={(e) => onChange({ ...config, provider_name: e.target.value })}
|
||||||
stroke="currentColor"
|
/>
|
||||||
>
|
<p className="text-muted-foreground text-xs">
|
||||||
<path
|
{t('setupPage.forms.apiProvider.providerName.description')}
|
||||||
strokeLinecap="round"
|
</p>
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-sm">
|
|
||||||
<p className="mb-1 font-medium text-blue-900 dark:text-blue-100">
|
|
||||||
{t('setupPage.forms.siliconFlow.about.title')}
|
|
||||||
</p>
|
|
||||||
<p className="mb-2 text-blue-700 dark:text-blue-300">
|
|
||||||
{t('setupPage.forms.siliconFlow.about.description')}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://cloud.siliconflow.cn"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 font-medium text-blue-600 hover:underline dark:text-blue-400"
|
|
||||||
>
|
|
||||||
{t('setupPage.forms.siliconFlow.about.link')}
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label htmlFor="siliconflow_api_key">{t('setupPage.forms.siliconFlow.apiKey.label')}</Label>
|
<Label htmlFor="base_url">{t('setupPage.forms.apiProvider.baseUrl.label')}</Label>
|
||||||
|
<Input
|
||||||
|
id="base_url"
|
||||||
|
placeholder="https://api.example.com/v1"
|
||||||
|
value={config.base_url}
|
||||||
|
onChange={(e) => onChange({ ...config, base_url: e.target.value })}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t('setupPage.forms.apiProvider.baseUrl.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="api_key">{t('setupPage.forms.apiProvider.apiKey.label')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id="siliconflow_api_key"
|
id="api_key"
|
||||||
type={showApiKey ? 'text' : 'password'}
|
type={showApiKey ? 'text' : 'password'}
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
value={config.api_key}
|
value={config.api_key}
|
||||||
onChange={(e) => onChange({ api_key: e.target.value })}
|
onChange={(e) => onChange({ ...config, api_key: e.target.value })}
|
||||||
className="pr-10 font-mono"
|
className="pr-10 font-mono"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -633,25 +351,103 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t('setupPage.forms.siliconFlow.apiKey.description')}
|
{t('setupPage.forms.apiProvider.apiKey.description')}
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-muted/50 space-y-2 rounded-lg p-4 text-sm">
|
|
||||||
<p className="font-medium">{t('setupPage.forms.siliconFlow.autoConfig.title')}</p>
|
|
||||||
<ul className="text-muted-foreground ml-2 list-inside list-disc space-y-1">
|
|
||||||
{autoConfigItems.map((item) => (
|
|
||||||
<li key={item}>{item}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/30">
|
|
||||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
|
||||||
<span className="font-medium">{t('setupPage.forms.siliconFlow.hint.title')}</span>
|
|
||||||
{t('setupPage.forms.siliconFlow.hint.description')}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== 步骤4:基础模型配置 ======
|
||||||
|
interface ModelSetupFormProps {
|
||||||
|
config: ModelSetupConfig
|
||||||
|
onChange: (config: ModelSetupConfig) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelSetupForm({ config, onChange }: ModelSetupFormProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="planner_model_identifier">
|
||||||
|
{t('setupPage.forms.modelSetup.planner.identifier.label')}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="planner_model_identifier"
|
||||||
|
placeholder="gpt-4.1-mini"
|
||||||
|
value={config.planner_model_identifier}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
planner_model_identifier: e.target.value,
|
||||||
|
planner_model_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t('setupPage.forms.modelSetup.planner.identifier.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-md bg-muted/40 p-3">
|
||||||
|
<Label htmlFor="planner_visual" className="text-sm font-medium">
|
||||||
|
{t('setupPage.forms.modelSetup.planner.visual.label')}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="planner_visual"
|
||||||
|
checked={config.planner_visual}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...config, planner_visual: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="replyer_model_identifier">
|
||||||
|
{t('setupPage.forms.modelSetup.replyer.identifier.label')}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="replyer_model_identifier"
|
||||||
|
placeholder="gpt-4.1"
|
||||||
|
value={config.replyer_model_identifier}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
replyer_model_identifier: e.target.value,
|
||||||
|
replyer_model_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t('setupPage.forms.modelSetup.replyer.identifier.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-md bg-muted/40 p-3">
|
||||||
|
<Label htmlFor="replyer_visual" className="text-sm font-medium">
|
||||||
|
{t('setupPage.forms.modelSetup.replyer.visual.label')}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="replyer_visual"
|
||||||
|
checked={config.replyer_visual}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...config, replyer_visual: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4 text-sm text-muted-foreground">
|
||||||
|
{t('setupPage.forms.modelSetup.saveHint')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,49 @@ import { parseResponse, throwIfError } from '@/lib/api-helpers'
|
|||||||
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
|
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiProviderSetupConfig,
|
||||||
BotBasicConfig,
|
BotBasicConfig,
|
||||||
EmojiConfig,
|
ModelSetupConfig,
|
||||||
OtherBasicConfig,
|
|
||||||
PersonalityConfig,
|
PersonalityConfig,
|
||||||
SiliconFlowConfig,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
interface ModelInfo {
|
||||||
|
model_identifier: string
|
||||||
|
name: string
|
||||||
|
api_provider: string
|
||||||
|
price_in?: number
|
||||||
|
cache?: boolean
|
||||||
|
cache_price_in?: number
|
||||||
|
price_out?: number
|
||||||
|
force_stream_mode?: boolean
|
||||||
|
visual?: boolean
|
||||||
|
extra_params?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiProviderConfig {
|
||||||
|
name: string
|
||||||
|
base_url: string
|
||||||
|
api_key: string
|
||||||
|
client_type?: string
|
||||||
|
max_retry?: number
|
||||||
|
timeout?: number
|
||||||
|
retry_interval?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskConfig {
|
||||||
|
model_list?: string[]
|
||||||
|
max_tokens?: number
|
||||||
|
temperature?: number
|
||||||
|
slow_threshold?: number
|
||||||
|
selection_strategy?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelConfig {
|
||||||
|
models?: ModelInfo[]
|
||||||
|
api_providers?: ApiProviderConfig[]
|
||||||
|
model_task_config?: Record<string, TaskConfig>
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 读取配置 =====
|
// ===== 读取配置 =====
|
||||||
|
|
||||||
// 读取Bot基础配置
|
// 读取Bot基础配置
|
||||||
@@ -56,73 +92,57 @@ export async function loadPersonalityConfig(): Promise<PersonalityConfig> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取表情包配置
|
async function loadModelConfig(): Promise<ModelConfig> {
|
||||||
export async function loadEmojiConfig(): Promise<EmojiConfig> {
|
|
||||||
const response = await fetchWithAuth('/api/webui/config/bot', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await parseResponse<{ config: { emoji?: EmojiConfig } }>(
|
|
||||||
response
|
|
||||||
)
|
|
||||||
const data = throwIfError(result)
|
|
||||||
const emojiConfig = (data.config.emoji || {}) as Partial<EmojiConfig>
|
|
||||||
|
|
||||||
return {
|
|
||||||
emoji_send_num: emojiConfig.emoji_send_num ?? 25,
|
|
||||||
max_reg_num: emojiConfig.max_reg_num ?? 64,
|
|
||||||
do_replace: emojiConfig.do_replace ?? true,
|
|
||||||
check_interval: emojiConfig.check_interval ?? 10,
|
|
||||||
steal_emoji: emojiConfig.steal_emoji ?? true,
|
|
||||||
content_filtration: emojiConfig.content_filtration ?? false,
|
|
||||||
filtration_prompt: emojiConfig.filtration_prompt || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取其他基础配置
|
|
||||||
export async function loadOtherBasicConfig(): Promise<OtherBasicConfig> {
|
|
||||||
const response = await fetchWithAuth('/api/webui/config/bot', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await parseResponse<{
|
|
||||||
config: {
|
|
||||||
expression?: { all_global_jargon?: boolean }
|
|
||||||
}
|
|
||||||
}>(response)
|
|
||||||
const data = throwIfError(result)
|
|
||||||
const config = data.config
|
|
||||||
|
|
||||||
const expressionConfig = config.expression || {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
all_global: expressionConfig.all_global_jargon ?? true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取硅基流动API配置
|
|
||||||
export async function loadSiliconFlowConfig(): Promise<SiliconFlowConfig> {
|
|
||||||
const response = await fetchWithAuth('/api/webui/config/model', {
|
const response = await fetchWithAuth('/api/webui/config/model', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await parseResponse<{
|
const result = await parseResponse<{ config: ModelConfig }>(response)
|
||||||
config: {
|
|
||||||
api_providers?: Array<{ name: string; api_key?: string }>
|
|
||||||
}
|
|
||||||
}>(response)
|
|
||||||
const data = throwIfError(result)
|
const data = throwIfError(result)
|
||||||
const modelConfig = data.config
|
return data.config || {}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取SiliconFlow提供商的API Key
|
// 读取 API 提供商配置
|
||||||
const apiProviders = modelConfig.api_providers || []
|
export async function loadApiProviderSetupConfig(): Promise<ApiProviderSetupConfig> {
|
||||||
const siliconFlowProvider = apiProviders.find((p) => p.name === 'SiliconFlow')
|
const modelConfig = await loadModelConfig()
|
||||||
|
const models = modelConfig.models || []
|
||||||
|
const taskConfig = modelConfig.model_task_config || {}
|
||||||
|
const plannerName = taskConfig.planner?.model_list?.[0] || ''
|
||||||
|
const replyerName = taskConfig.replyer?.model_list?.[0] || ''
|
||||||
|
const plannerModel = models.find((model) => model.name === plannerName)
|
||||||
|
const replyerModel = models.find((model) => model.name === replyerName)
|
||||||
|
const providerName =
|
||||||
|
plannerModel?.api_provider ||
|
||||||
|
replyerModel?.api_provider ||
|
||||||
|
modelConfig.api_providers?.[0]?.name ||
|
||||||
|
''
|
||||||
|
const provider = modelConfig.api_providers?.find((item) => item.name === providerName)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api_key: siliconFlowProvider?.api_key || '',
|
provider_name: providerName,
|
||||||
|
base_url: provider?.base_url || '',
|
||||||
|
api_key: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取基础模型配置
|
||||||
|
export async function loadModelSetupConfig(): Promise<ModelSetupConfig> {
|
||||||
|
const modelConfig = await loadModelConfig()
|
||||||
|
const models = modelConfig.models || []
|
||||||
|
const taskConfig = modelConfig.model_task_config || {}
|
||||||
|
const plannerName = taskConfig.planner?.model_list?.[0] || ''
|
||||||
|
const replyerName = taskConfig.replyer?.model_list?.[0] || ''
|
||||||
|
const plannerModel = models.find((model) => model.name === plannerName)
|
||||||
|
const replyerModel = models.find((model) => model.name === replyerName)
|
||||||
|
|
||||||
|
return {
|
||||||
|
planner_model_name: plannerName,
|
||||||
|
planner_model_identifier: plannerModel?.model_identifier || plannerName,
|
||||||
|
planner_visual: Boolean(plannerModel?.visual),
|
||||||
|
replyer_model_name: replyerName,
|
||||||
|
replyer_model_identifier: replyerModel?.model_identifier || replyerName,
|
||||||
|
replyer_visual: Boolean(replyerModel?.visual),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,19 +163,6 @@ export async function saveBotBasicConfig(config: BotBasicConfig) {
|
|||||||
// 保存人格配置
|
// 保存人格配置
|
||||||
export async function savePersonalityConfig(config: PersonalityConfig) {
|
export async function savePersonalityConfig(config: PersonalityConfig) {
|
||||||
const response = await fetchWithAuth('/api/webui/config/bot/section/personality', {
|
const response = await fetchWithAuth('/api/webui/config/bot/section/personality', {
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(config),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await parseResponse(response)
|
|
||||||
return throwIfError(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存表情包配置
|
|
||||||
export async function saveEmojiConfig(config: EmojiConfig) {
|
|
||||||
const response = await fetchWithAuth('/api/webui/config/bot/section/emoji', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
@@ -165,58 +172,62 @@ export async function saveEmojiConfig(config: EmojiConfig) {
|
|||||||
return throwIfError(result)
|
return throwIfError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存其他基础配置(黑话)
|
function createBasicModel(
|
||||||
export async function saveOtherBasicConfig(config: OtherBasicConfig) {
|
modelName: string,
|
||||||
const response = await fetchWithAuth('/api/webui/config/bot/section/expression', {
|
modelIdentifier: string,
|
||||||
method: 'POST',
|
providerName: string,
|
||||||
headers: getAuthHeaders(),
|
visual: boolean,
|
||||||
body: JSON.stringify({ all_global_jargon: config.all_global }),
|
existing?: ModelInfo
|
||||||
})
|
): ModelInfo {
|
||||||
|
return {
|
||||||
const result = await parseResponse(response)
|
price_in: 0,
|
||||||
return throwIfError(result)
|
cache: false,
|
||||||
|
cache_price_in: 0,
|
||||||
|
price_out: 0,
|
||||||
|
force_stream_mode: false,
|
||||||
|
extra_params: {},
|
||||||
|
...existing,
|
||||||
|
visual,
|
||||||
|
model_identifier: modelIdentifier,
|
||||||
|
name: modelName,
|
||||||
|
api_provider: providerName,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存硅基流动API配置
|
function upsertModel(models: ModelInfo[], model: ModelInfo): ModelInfo[] {
|
||||||
export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
|
const index = models.findIndex((item) => item.name === model.name)
|
||||||
// 1. 读取现有配置
|
if (index >= 0) {
|
||||||
const response = await fetchWithAuth('/api/webui/config/model', {
|
return models.map((item, itemIndex) => (itemIndex === index ? model : item))
|
||||||
method: 'GET',
|
}
|
||||||
headers: getAuthHeaders(),
|
return [...models, model]
|
||||||
})
|
}
|
||||||
|
|
||||||
const result = await parseResponse<{
|
// 保存 API 提供商配置
|
||||||
config: {
|
export async function saveApiProviderSetupConfig(config: ApiProviderSetupConfig) {
|
||||||
api_providers?: Array<Record<string, unknown>>
|
const modelConfig = await loadModelConfig()
|
||||||
}
|
const providerName = config.provider_name.trim()
|
||||||
}>(response)
|
|
||||||
const currentModelConfig = throwIfError(result)
|
|
||||||
const modelConfig = currentModelConfig.config
|
|
||||||
|
|
||||||
// 2. 更新SiliconFlow提供商的API Key
|
|
||||||
const apiProviders = modelConfig.api_providers || []
|
const apiProviders = modelConfig.api_providers || []
|
||||||
const siliconFlowIndex = apiProviders.findIndex((p) => p.name === 'SiliconFlow')
|
const providerIndex = apiProviders.findIndex((provider) => provider.name === providerName)
|
||||||
|
const providerConfig: ApiProviderConfig = {
|
||||||
if (siliconFlowIndex >= 0) {
|
name: providerName,
|
||||||
// 更新现有提供商的API Key
|
base_url: config.base_url.trim(),
|
||||||
apiProviders[siliconFlowIndex] = {
|
api_key: config.api_key.trim(),
|
||||||
...apiProviders[siliconFlowIndex],
|
client_type: 'openai',
|
||||||
api_key: config.api_key,
|
max_retry: 3,
|
||||||
}
|
timeout: 120,
|
||||||
} else {
|
retry_interval: 5,
|
||||||
// 如果不存在,创建新的SiliconFlow提供商
|
}
|
||||||
apiProviders.push({
|
|
||||||
name: 'SiliconFlow',
|
if (providerIndex >= 0) {
|
||||||
base_url: 'https://api.siliconflow.cn/v1',
|
apiProviders[providerIndex] = {
|
||||||
api_key: config.api_key,
|
...apiProviders[providerIndex],
|
||||||
client_type: 'openai',
|
...providerConfig,
|
||||||
max_retry: 3,
|
}
|
||||||
timeout: 120,
|
} else {
|
||||||
retry_interval: 5,
|
apiProviders.push(providerConfig)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 保存更新后的配置
|
|
||||||
const updatedConfig = {
|
const updatedConfig = {
|
||||||
...modelConfig,
|
...modelConfig,
|
||||||
api_providers: apiProviders,
|
api_providers: apiProviders,
|
||||||
@@ -232,6 +243,77 @@ export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
|
|||||||
return throwIfError(saveResult)
|
return throwIfError(saveResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存基础模型配置
|
||||||
|
export async function saveModelSetupConfig(
|
||||||
|
config: ModelSetupConfig,
|
||||||
|
providerName: string
|
||||||
|
) {
|
||||||
|
const modelConfig = await loadModelConfig()
|
||||||
|
const trimmedProviderName = providerName.trim()
|
||||||
|
const plannerModelIdentifier = config.planner_model_identifier.trim()
|
||||||
|
const plannerModelName = plannerModelIdentifier
|
||||||
|
const replyerModelIdentifier = config.replyer_model_identifier.trim()
|
||||||
|
const replyerModelName = replyerModelIdentifier
|
||||||
|
|
||||||
|
// 新增或更新 planner/replyer 模型,并仅同步 utils 到 planner。
|
||||||
|
let models = modelConfig.models || []
|
||||||
|
const existingPlannerModel = models.find((model) => model.name === plannerModelName)
|
||||||
|
const existingReplyerModel = models.find((model) => model.name === replyerModelName)
|
||||||
|
models = upsertModel(
|
||||||
|
models,
|
||||||
|
createBasicModel(
|
||||||
|
plannerModelName,
|
||||||
|
plannerModelIdentifier,
|
||||||
|
trimmedProviderName,
|
||||||
|
config.planner_visual,
|
||||||
|
existingPlannerModel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
models = upsertModel(
|
||||||
|
models,
|
||||||
|
createBasicModel(
|
||||||
|
replyerModelName,
|
||||||
|
replyerModelIdentifier,
|
||||||
|
trimmedProviderName,
|
||||||
|
config.replyer_visual,
|
||||||
|
existingReplyerModel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const modelTaskConfig = modelConfig.model_task_config || {}
|
||||||
|
const updatedTaskConfig = {
|
||||||
|
...modelTaskConfig,
|
||||||
|
planner: {
|
||||||
|
...(modelTaskConfig.planner || {}),
|
||||||
|
model_list: [plannerModelName],
|
||||||
|
},
|
||||||
|
replyer: {
|
||||||
|
...(modelTaskConfig.replyer || {}),
|
||||||
|
model_list: [replyerModelName],
|
||||||
|
},
|
||||||
|
utils: {
|
||||||
|
...(modelTaskConfig.utils || {}),
|
||||||
|
model_list: [plannerModelName],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// vlm/voice/embedding 等其他任务配置保持原样。
|
||||||
|
const updatedConfig = {
|
||||||
|
...modelConfig,
|
||||||
|
models,
|
||||||
|
model_task_config: updatedTaskConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveResponse = await fetchWithAuth('/api/webui/config/model', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(updatedConfig),
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveResult = await parseResponse(saveResponse)
|
||||||
|
return throwIfError(saveResult)
|
||||||
|
}
|
||||||
|
|
||||||
// 标记设置完成
|
// 标记设置完成
|
||||||
export async function completeSetup() {
|
export async function completeSetup() {
|
||||||
const response = await fetchWithAuth('/api/webui/setup/complete', {
|
const response = await fetchWithAuth('/api/webui/setup/complete', {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Brain,
|
||||||
Bot,
|
Bot,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Globe,
|
Globe,
|
||||||
Key,
|
Key,
|
||||||
Settings,
|
|
||||||
SkipForward,
|
SkipForward,
|
||||||
Smile,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -38,31 +37,27 @@ import { cn } from '@/lib/utils'
|
|||||||
import { APP_NAME } from '@/lib/version'
|
import { APP_NAME } from '@/lib/version'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
import type {
|
import type {
|
||||||
|
ApiProviderSetupConfig,
|
||||||
SetupStep,
|
SetupStep,
|
||||||
BotBasicConfig,
|
BotBasicConfig,
|
||||||
|
ModelSetupConfig,
|
||||||
PersonalityConfig,
|
PersonalityConfig,
|
||||||
EmojiConfig,
|
|
||||||
OtherBasicConfig,
|
|
||||||
SiliconFlowConfig,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
import {
|
import {
|
||||||
|
ApiProviderSetupForm,
|
||||||
BotBasicForm,
|
BotBasicForm,
|
||||||
|
ModelSetupForm,
|
||||||
PersonalityForm,
|
PersonalityForm,
|
||||||
EmojiForm,
|
|
||||||
OtherBasicForm,
|
|
||||||
SiliconFlowForm,
|
|
||||||
} from './StepForms'
|
} from './StepForms'
|
||||||
import {
|
import {
|
||||||
loadBotBasicConfig,
|
loadBotBasicConfig,
|
||||||
loadPersonalityConfig,
|
loadPersonalityConfig,
|
||||||
loadEmojiConfig,
|
loadApiProviderSetupConfig,
|
||||||
loadOtherBasicConfig,
|
loadModelSetupConfig,
|
||||||
loadSiliconFlowConfig,
|
|
||||||
saveBotBasicConfig,
|
saveBotBasicConfig,
|
||||||
savePersonalityConfig,
|
savePersonalityConfig,
|
||||||
saveEmojiConfig,
|
saveApiProviderSetupConfig,
|
||||||
saveOtherBasicConfig,
|
saveModelSetupConfig,
|
||||||
saveSiliconFlowConfig,
|
|
||||||
completeSetup,
|
completeSetup,
|
||||||
} from './api'
|
} from './api'
|
||||||
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||||
@@ -103,15 +98,6 @@ function SetupPageContent() {
|
|||||||
],
|
],
|
||||||
multiple_probability: 0.2,
|
multiple_probability: 0.2,
|
||||||
})
|
})
|
||||||
const createDefaultEmojiConfig = (): EmojiConfig => ({
|
|
||||||
emoji_send_num: 25,
|
|
||||||
max_reg_num: 64,
|
|
||||||
do_replace: true,
|
|
||||||
check_interval: 10,
|
|
||||||
steal_emoji: true,
|
|
||||||
content_filtration: false,
|
|
||||||
filtration_prompt: t('setupPage.defaults.emoji.filtrationPrompt'),
|
|
||||||
})
|
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
const [isCompleting, setIsCompleting] = useState(false)
|
const [isCompleting, setIsCompleting] = useState(false)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
@@ -131,17 +117,21 @@ function SetupPageContent() {
|
|||||||
createDefaultPersonalityConfig()
|
createDefaultPersonalityConfig()
|
||||||
)
|
)
|
||||||
|
|
||||||
// 步骤3:表情包配置
|
// 步骤3:API 提供商配置
|
||||||
const [emoji, setEmoji] = useState<EmojiConfig>(() => createDefaultEmojiConfig())
|
const [apiProviderSetup, setApiProviderSetup] = useState<ApiProviderSetupConfig>({
|
||||||
|
provider_name: '',
|
||||||
// 步骤4:其他基础配置
|
base_url: '',
|
||||||
const [otherBasic, setOtherBasic] = useState<OtherBasicConfig>({
|
api_key: '',
|
||||||
all_global: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 步骤5:硅基流动API配置
|
// 步骤4:基础模型配置
|
||||||
const [siliconFlow, setSiliconFlow] = useState<SiliconFlowConfig>({
|
const [modelSetup, setModelSetup] = useState<ModelSetupConfig>({
|
||||||
api_key: '',
|
planner_model_name: '',
|
||||||
|
planner_model_identifier: '',
|
||||||
|
planner_visual: false,
|
||||||
|
replyer_model_name: '',
|
||||||
|
replyer_model_identifier: '',
|
||||||
|
replyer_visual: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const steps: SetupStep[] = [
|
const steps: SetupStep[] = [
|
||||||
@@ -158,23 +148,17 @@ function SetupPageContent() {
|
|||||||
icon: User,
|
icon: User,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'emoji',
|
id: 'api-provider',
|
||||||
title: t('setupPage.steps.emoji.title'),
|
title: t('setupPage.steps.apiProvider.title'),
|
||||||
description: t('setupPage.steps.emoji.description'),
|
description: t('setupPage.steps.apiProvider.description'),
|
||||||
icon: Smile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'other',
|
|
||||||
title: t('setupPage.steps.other.title'),
|
|
||||||
description: t('setupPage.steps.other.description'),
|
|
||||||
icon: Settings,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'siliconflow',
|
|
||||||
title: t('setupPage.steps.siliconFlow.title'),
|
|
||||||
description: t('setupPage.steps.siliconFlow.description'),
|
|
||||||
icon: Key,
|
icon: Key,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'model-setup',
|
||||||
|
title: t('setupPage.steps.modelSetup.title'),
|
||||||
|
description: t('setupPage.steps.modelSetup.description'),
|
||||||
|
icon: Brain,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const progress = ((currentStep + 1) / steps.length) * 100
|
const progress = ((currentStep + 1) / steps.length) * 100
|
||||||
@@ -186,19 +170,17 @@ function SetupPageContent() {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// 并行加载所有配置
|
// 并行加载所有配置
|
||||||
const [bot, personality, emoji, other, silicon] = await Promise.all([
|
const [bot, personality, apiProvider, model] = await Promise.all([
|
||||||
loadBotBasicConfig(),
|
loadBotBasicConfig(),
|
||||||
loadPersonalityConfig(),
|
loadPersonalityConfig(),
|
||||||
loadEmojiConfig(),
|
loadApiProviderSetupConfig(),
|
||||||
loadOtherBasicConfig(),
|
loadModelSetupConfig(),
|
||||||
loadSiliconFlowConfig(),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
setBotBasic(bot)
|
setBotBasic(bot)
|
||||||
setPersonality(personality)
|
setPersonality(personality)
|
||||||
setEmoji(emoji)
|
setApiProviderSetup(apiProvider)
|
||||||
setOtherBasic(other)
|
setModelSetup(model)
|
||||||
setSiliconFlow(silicon)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: t('setupPage.toast.loadFailedTitle'),
|
title: t('setupPage.toast.loadFailedTitle'),
|
||||||
@@ -225,14 +207,11 @@ function SetupPageContent() {
|
|||||||
case 1: // 人格配置
|
case 1: // 人格配置
|
||||||
await savePersonalityConfig(personality)
|
await savePersonalityConfig(personality)
|
||||||
break
|
break
|
||||||
case 2: // 表情包
|
case 2: // API 提供商
|
||||||
await saveEmojiConfig(emoji)
|
await saveApiProviderSetupConfig(apiProviderSetup)
|
||||||
break
|
break
|
||||||
case 3: // 其他设置
|
case 3: // 基础模型
|
||||||
await saveOtherBasicConfig(otherBasic)
|
await saveModelSetupConfig(modelSetup, apiProviderSetup.provider_name)
|
||||||
break
|
|
||||||
case 4: // 硅基流动API
|
|
||||||
await saveSiliconFlowConfig(siliconFlow)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +251,24 @@ function SetupPageContent() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateApiProviderSetup(config: ApiProviderSetupConfig): string | null {
|
||||||
|
if (!config.provider_name.trim()) return t('setupPage.validation.enterProviderName')
|
||||||
|
if (!config.base_url.trim()) return t('setupPage.validation.enterBaseUrl')
|
||||||
|
if (!config.api_key.trim()) return t('setupPage.validation.enterApiKey')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateModelSetup(config: ModelSetupConfig): string | null {
|
||||||
|
if (!config.planner_model_identifier.trim()) {
|
||||||
|
return t('setupPage.validation.enterPlannerModelIdentifier')
|
||||||
|
}
|
||||||
|
if (!config.replyer_model_identifier.trim()) {
|
||||||
|
return t('setupPage.validation.enterReplyerModelIdentifier')
|
||||||
|
}
|
||||||
|
if (!apiProviderSetup.provider_name.trim()) return t('setupPage.validation.enterProviderName')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
// Step 1 验证
|
// Step 1 验证
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
@@ -285,6 +282,28 @@ function SetupPageContent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (currentStep === 2) {
|
||||||
|
const error = validateApiProviderSetup(apiProviderSetup)
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t('setupPage.toast.validationFailedTitle'),
|
||||||
|
description: error,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentStep === 3) {
|
||||||
|
const error = validateModelSetup(modelSetup)
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t('setupPage.toast.validationFailedTitle'),
|
||||||
|
description: error,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存当前步骤
|
// 保存当前步骤
|
||||||
const saved = await saveCurrentStep()
|
const saved = await saveCurrentStep()
|
||||||
@@ -306,7 +325,18 @@ function SetupPageContent() {
|
|||||||
setIsCompleting(true)
|
setIsCompleting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 保存最后一步的配置(硅基流动API Key)
|
const error = validateModelSetup(modelSetup)
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t('setupPage.toast.validationFailedTitle'),
|
||||||
|
description: error,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
setIsCompleting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 保存最后一步的基础模型配置
|
||||||
const saved = await saveCurrentStep()
|
const saved = await saveCurrentStep()
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
setIsCompleting(false)
|
setIsCompleting(false)
|
||||||
@@ -357,11 +387,9 @@ function SetupPageContent() {
|
|||||||
case 1:
|
case 1:
|
||||||
return <PersonalityForm config={personality} onChange={setPersonality} />
|
return <PersonalityForm config={personality} onChange={setPersonality} />
|
||||||
case 2:
|
case 2:
|
||||||
return <EmojiForm config={emoji} onChange={setEmoji} />
|
return <ApiProviderSetupForm config={apiProviderSetup} onChange={setApiProviderSetup} />
|
||||||
case 3:
|
case 3:
|
||||||
return <OtherBasicForm config={otherBasic} onChange={setOtherBasic} />
|
return <ModelSetupForm config={modelSetup} onChange={setModelSetup} />
|
||||||
case 4:
|
|
||||||
return <SiliconFlowForm config={siliconFlow} onChange={setSiliconFlow} />
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,23 +24,19 @@ export interface PersonalityConfig {
|
|||||||
multiple_probability: number
|
multiple_probability: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤3:表情包配置
|
// 步骤3:API 提供商配置
|
||||||
export interface EmojiConfig {
|
export interface ApiProviderSetupConfig {
|
||||||
emoji_send_num: number
|
provider_name: string
|
||||||
max_reg_num: number
|
base_url: string
|
||||||
do_replace: boolean
|
|
||||||
check_interval: number
|
|
||||||
steal_emoji: boolean
|
|
||||||
content_filtration: boolean
|
|
||||||
filtration_prompt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤4:其他基础配置
|
|
||||||
export interface OtherBasicConfig {
|
|
||||||
all_global: boolean // 全局黑话模式(expression.all_global_jargon)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤5:硅基流动API配置
|
|
||||||
export interface SiliconFlowConfig {
|
|
||||||
api_key: string
|
api_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 步骤4:基础模型配置
|
||||||
|
export interface ModelSetupConfig {
|
||||||
|
planner_model_name: string
|
||||||
|
planner_model_identifier: string
|
||||||
|
planner_visual: boolean
|
||||||
|
replyer_model_name: string
|
||||||
|
replyer_model_identifier: string
|
||||||
|
replyer_visual: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ export interface PluginManifest {
|
|||||||
homepage_url?: string
|
homepage_url?: string
|
||||||
/** 插件仓库地址(可选) */
|
/** 插件仓库地址(可选) */
|
||||||
repository_url?: string
|
repository_url?: string
|
||||||
|
/** Manifest v2 URL 集合(可选) */
|
||||||
|
urls?: {
|
||||||
|
repository?: string
|
||||||
|
homepage?: string
|
||||||
|
documentation?: string
|
||||||
|
issues?: string
|
||||||
|
}
|
||||||
/** 插件关键词 */
|
/** 插件关键词 */
|
||||||
keywords: string[]
|
keywords: string[]
|
||||||
/** 插件分类(可选) */
|
/** 插件分类(可选) */
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export default defineConfig({
|
|||||||
cookieDomainRewrite: '', // 移除域名限制
|
cookieDomainRewrite: '', // 移除域名限制
|
||||||
cookiePathRewrite: '/', // 确保路径一致
|
cookiePathRewrite: '/', // 确保路径一致
|
||||||
},
|
},
|
||||||
|
'/maibot_statistics.html': {
|
||||||
|
target: 'http://127.0.0.1:8001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ dependencies = [
|
|||||||
"jieba>=0.42.1",
|
"jieba>=0.42.1",
|
||||||
"json-repair>=0.47.6",
|
"json-repair>=0.47.6",
|
||||||
"maim-message>=0.6.2",
|
"maim-message>=0.6.2",
|
||||||
"maibot-dashboard==1.0.1.dev2026050251",
|
"maibot-dashboard>=1.0.2.dev2026050359",
|
||||||
"maibot-plugin-sdk>=2.4.0",
|
"maibot-plugin-sdk>=2.4.0",
|
||||||
"matplotlib>=3.10.5",
|
"matplotlib>=3.10.5",
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|||||||
@@ -995,6 +995,14 @@ class TestManifestValidator:
|
|||||||
assert len(validator.errors) == 0
|
assert len(validator.errors) == 0
|
||||||
assert validator.warnings == []
|
assert validator.warnings == []
|
||||||
|
|
||||||
|
def test_manifest_id_allows_uppercase_and_underscore(self):
|
||||||
|
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
||||||
|
|
||||||
|
validator = ManifestValidator(host_version="1.0.0", sdk_version="2.0.1")
|
||||||
|
manifest = build_test_manifest("XXXxx7258.google_search_plugin", capabilities=["send.text"])
|
||||||
|
assert validator.validate(manifest) is True
|
||||||
|
assert validator.errors == []
|
||||||
|
|
||||||
def test_missing_required_fields(self):
|
def test_missing_required_fields(self):
|
||||||
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute(
|
|||||||
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
|
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
|
||||||
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
|
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
|
||||||
MMC_VERSION: str = "1.0.0"
|
MMC_VERSION: str = "1.0.0"
|
||||||
CONFIG_VERSION: str = "8.9.21"
|
CONFIG_VERSION: str = "8.10.1"
|
||||||
MODEL_CONFIG_VERSION: str = "1.14.6"
|
MODEL_CONFIG_VERSION: str = "1.14.6"
|
||||||
|
|
||||||
logger = get_logger("config")
|
logger = get_logger("config")
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class BotConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "custom",
|
"x-widget": "custom",
|
||||||
"x-icon": "tags",
|
"x-icon": "tags",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""别名列表"""
|
"""别名列表"""
|
||||||
@@ -472,6 +473,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "message-circle-warning",
|
"x-icon": "message-circle-warning",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否启用反馈驱动的延迟记忆纠错任务"""
|
"""是否启用反馈驱动的延迟记忆纠错任务"""
|
||||||
@@ -482,6 +484,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "clock-4",
|
"x-icon": "clock-4",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""反馈窗口时长(小时),以 query_memory 执行时间为起点"""
|
"""反馈窗口时长(小时),以 query_memory 执行时间为起点"""
|
||||||
@@ -492,6 +495,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "timer",
|
"x-icon": "timer",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""反馈纠错定时任务轮询间隔(分钟)"""
|
"""反馈纠错定时任务轮询间隔(分钟)"""
|
||||||
@@ -503,6 +507,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "list-ordered",
|
"x-icon": "list-ordered",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""反馈纠错每轮最大处理任务数"""
|
"""反馈纠错每轮最大处理任务数"""
|
||||||
@@ -515,6 +520,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
"x-widget": "slider",
|
"x-widget": "slider",
|
||||||
"x-icon": "gauge",
|
"x-icon": "gauge",
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""自动应用纠错动作的最低置信度阈值"""
|
"""自动应用纠错动作的最低置信度阈值"""
|
||||||
@@ -526,6 +532,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "messages-square",
|
"x-icon": "messages-square",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""每个纠错任务最多使用的窗口内用户反馈消息数"""
|
"""每个纠错任务最多使用的窗口内用户反馈消息数"""
|
||||||
@@ -535,6 +542,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "filter",
|
"x-icon": "filter",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否启用纠错前置预筛(用于减少不必要的模型调用)"""
|
"""是否启用纠错前置预筛(用于减少不必要的模型调用)"""
|
||||||
@@ -544,6 +552,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "sticky-note",
|
"x-icon": "sticky-note",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否为受影响 paragraph 写入已纠正旧事实标记"""
|
"""是否为受影响 paragraph 写入已纠正旧事实标记"""
|
||||||
@@ -553,6 +562,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "eye-off",
|
"x-icon": "eye-off",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否在用户侧查询中硬过滤带有 stale 标记的 paragraph"""
|
"""是否在用户侧查询中硬过滤带有 stale 标记的 paragraph"""
|
||||||
@@ -562,6 +572,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "user-round-search",
|
"x-icon": "user-round-search",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否在反馈纠错后将受影响人物画像加入刷新队列"""
|
"""是否在反馈纠错后将受影响人物画像加入刷新队列"""
|
||||||
@@ -571,6 +582,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "refresh-ccw",
|
"x-icon": "refresh-ccw",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""人物画像处于脏队列时,读取是否强制刷新而不直接复用旧快照"""
|
"""人物画像处于脏队列时,读取是否强制刷新而不直接复用旧快照"""
|
||||||
@@ -580,6 +592,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "clapperboard",
|
"x-icon": "clapperboard",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""是否在反馈纠错后将受影响 source 加入 episode 重建队列"""
|
"""是否在反馈纠错后将受影响 source 加入 episode 重建队列"""
|
||||||
@@ -589,6 +602,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "switch",
|
"x-widget": "switch",
|
||||||
"x-icon": "ban",
|
"x-icon": "ban",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""episode source 处于重建队列时,是否对用户侧查询做屏蔽"""
|
"""episode source 处于重建队列时,是否对用户侧查询做屏蔽"""
|
||||||
@@ -599,6 +613,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "repeat",
|
"x-icon": "repeat",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""反馈纠错二阶段一致性后台协调任务轮询间隔(分钟)"""
|
"""反馈纠错二阶段一致性后台协调任务轮询间隔(分钟)"""
|
||||||
@@ -610,6 +625,7 @@ class MemoryConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "list-restart",
|
"x-icon": "list-restart",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""反馈纠错二阶段一致性每轮处理 profile/episode 队列的批大小"""
|
"""反馈纠错二阶段一致性每轮处理 profile/episode 队列的批大小"""
|
||||||
@@ -1350,6 +1366,7 @@ class ChineseTypoConfig(ConfigBase):
|
|||||||
"x-widget": "slider",
|
"x-widget": "slider",
|
||||||
"x-icon": "percent",
|
"x-icon": "percent",
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""单字替换概率"""
|
"""单字替换概率"""
|
||||||
@@ -1359,6 +1376,7 @@ class ChineseTypoConfig(ConfigBase):
|
|||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
"x-icon": "hash",
|
"x-icon": "hash",
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""最小字频阈值"""
|
"""最小字频阈值"""
|
||||||
@@ -1371,6 +1389,7 @@ class ChineseTypoConfig(ConfigBase):
|
|||||||
"x-widget": "slider",
|
"x-widget": "slider",
|
||||||
"x-icon": "percent",
|
"x-icon": "percent",
|
||||||
"step": 0.1,
|
"step": 0.1,
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""声调错误概率"""
|
"""声调错误概率"""
|
||||||
@@ -1383,6 +1402,7 @@ class ChineseTypoConfig(ConfigBase):
|
|||||||
"x-widget": "slider",
|
"x-widget": "slider",
|
||||||
"x-icon": "percent",
|
"x-icon": "percent",
|
||||||
"step": 0.001,
|
"step": 0.001,
|
||||||
|
"advanced": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""整词替换概率"""
|
"""整词替换概率"""
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ def _serialize_messages(messages: List[Any]) -> List[Dict[str, Any]]:
|
|||||||
return [_serialize_message(message) for message in messages]
|
return [_serialize_message(message) for message in messages]
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_session_identity(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""为监控事件补充会话展示所需的群/用户标识。"""
|
||||||
|
|
||||||
|
session_id = data.get("session_id")
|
||||||
|
if not session_id:
|
||||||
|
return data
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.chat.message_receive.chat_manager import chat_manager
|
||||||
|
|
||||||
|
chat_stream = chat_manager.get_session_by_session_id(str(session_id))
|
||||||
|
except Exception:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if chat_stream is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
session_name = chat_manager.get_session_name(str(session_id))
|
||||||
|
if session_name:
|
||||||
|
data.setdefault("session_name", session_name)
|
||||||
|
data.setdefault("is_group_chat", chat_stream.is_group_session)
|
||||||
|
data.setdefault("group_id", chat_stream.group_id)
|
||||||
|
data.setdefault("user_id", chat_stream.user_id)
|
||||||
|
data.setdefault("platform", chat_stream.platform)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def _serialize_tool_results(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _serialize_tool_results(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""标准化最终 planner 卡中的工具结果列表。"""
|
"""标准化最终 planner 卡中的工具结果列表。"""
|
||||||
|
|
||||||
@@ -266,6 +293,7 @@ async def _broadcast(event: str, data: Dict[str, Any]) -> None:
|
|||||||
try:
|
try:
|
||||||
from src.webui.routers.websocket.manager import websocket_manager
|
from src.webui.routers.websocket.manager import websocket_manager
|
||||||
|
|
||||||
|
data = _enrich_session_identity(data)
|
||||||
subscription_key = f"{MONITOR_DOMAIN}:{MONITOR_TOPIC}"
|
subscription_key = f"{MONITOR_DOMAIN}:{MONITOR_TOPIC}"
|
||||||
total_connections = len(websocket_manager.connections)
|
total_connections = len(websocket_manager.connections)
|
||||||
subscriber_count = sum(
|
subscriber_count = sum(
|
||||||
@@ -291,12 +319,24 @@ async def _broadcast(event: str, data: Dict[str, Any]) -> None:
|
|||||||
logger.warning(f"MaiSaka 监控事件广播失败: {exc}", exc_info=True)
|
logger.warning(f"MaiSaka 监控事件广播失败: {exc}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
async def emit_session_start(session_id: str, session_name: str) -> None:
|
async def emit_session_start(
|
||||||
|
session_id: str,
|
||||||
|
session_name: str,
|
||||||
|
*,
|
||||||
|
is_group_chat: bool,
|
||||||
|
group_id: Optional[str],
|
||||||
|
user_id: Optional[str],
|
||||||
|
platform: str,
|
||||||
|
) -> None:
|
||||||
"""广播会话开始事件。"""
|
"""广播会话开始事件。"""
|
||||||
|
|
||||||
await _broadcast("session.start", {
|
await _broadcast("session.start", {
|
||||||
"session_id": session_id,
|
"session_id": session_id,
|
||||||
"session_name": session_name,
|
"session_name": session_name,
|
||||||
|
"is_group_chat": is_group_chat,
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"platform": platform,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .display.display_utils import build_tool_call_summary_lines, format_token_c
|
|||||||
from .display.prompt_cli_renderer import PromptCLIVisualizer
|
from .display.prompt_cli_renderer import PromptCLIVisualizer
|
||||||
from .display.stage_status_board import remove_stage_status, update_stage_status
|
from .display.stage_status_board import remove_stage_status, update_stage_status
|
||||||
from .history_utils import drop_leading_orphan_tool_results
|
from .history_utils import drop_leading_orphan_tool_results
|
||||||
|
from .monitor_events import emit_session_start
|
||||||
from .reasoning_engine import MaisakaReasoningEngine
|
from .reasoning_engine import MaisakaReasoningEngine
|
||||||
from .reply_effect import ReplyEffectTracker
|
from .reply_effect import ReplyEffectTracker
|
||||||
from .reply_effect.image_utils import extract_visual_attachments_from_sequence
|
from .reply_effect.image_utils import extract_visual_attachments_from_sequence
|
||||||
@@ -136,6 +137,7 @@ class MaisakaHeartFlowChatting:
|
|||||||
self._jargon_miner = JargonMiner(session_id, session_name=session_name)
|
self._jargon_miner = JargonMiner(session_id, session_name=session_name)
|
||||||
|
|
||||||
self._reasoning_engine = MaisakaReasoningEngine(self)
|
self._reasoning_engine = MaisakaReasoningEngine(self)
|
||||||
|
self._monitor_session_start_task: Optional[asyncio.Task[None]] = None
|
||||||
self._tool_registry = ToolRegistry()
|
self._tool_registry = ToolRegistry()
|
||||||
self._reply_effect_tracker = ReplyEffectTracker(
|
self._reply_effect_tracker = ReplyEffectTracker(
|
||||||
session_id=self.session_id,
|
session_id=self.session_id,
|
||||||
@@ -144,6 +146,24 @@ class MaisakaHeartFlowChatting:
|
|||||||
judge_runner=self._run_reply_effect_judge,
|
judge_runner=self._run_reply_effect_judge,
|
||||||
)
|
)
|
||||||
self._register_tool_providers()
|
self._register_tool_providers()
|
||||||
|
self._emit_monitor_session_start()
|
||||||
|
|
||||||
|
def _emit_monitor_session_start(self) -> None:
|
||||||
|
"""向 WebUI 监控面板同步当前会话的展示标识。"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._monitor_session_start_task = asyncio.create_task(
|
||||||
|
emit_session_start(
|
||||||
|
session_id=self.session_id,
|
||||||
|
session_name=self.session_name,
|
||||||
|
is_group_chat=self.chat_stream.is_group_session,
|
||||||
|
group_id=self.chat_stream.group_id,
|
||||||
|
user_id=self.chat_stream.user_id,
|
||||||
|
platform=self.chat_stream.platform,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except RuntimeError:
|
||||||
|
logger.debug("MaiSaka 监控会话开始事件未发送:当前没有运行中的事件循环")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_reply_effect_tracking_enabled() -> bool:
|
def _is_reply_effect_tracking_enabled() -> bool:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from src.common.logger import get_logger
|
|||||||
logger = get_logger("plugin_runtime.runner.manifest_validator")
|
logger = get_logger("plugin_runtime.runner.manifest_validator")
|
||||||
|
|
||||||
_SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
|
_SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
|
||||||
_PLUGIN_ID_PATTERN = re.compile(r"^[a-z0-9]+(?:[.-][a-z0-9]+)+$")
|
_PLUGIN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)+$")
|
||||||
_PACKAGE_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
_PACKAGE_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||||
_HTTP_URL_PATTERN = re.compile(r"^https?://.+$")
|
_HTTP_URL_PATTERN = re.compile(r"^https?://.+$")
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ class PluginDependencyDefinition(_StrictManifestModel):
|
|||||||
ValueError: 当 ID 不符合规则时抛出。
|
ValueError: 当 ID 不符合规则时抛出。
|
||||||
"""
|
"""
|
||||||
if not _PLUGIN_ID_PATTERN.fullmatch(value):
|
if not _PLUGIN_ID_PATTERN.fullmatch(value):
|
||||||
raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
|
raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator("version_spec")
|
@field_validator("version_spec")
|
||||||
@@ -548,7 +548,7 @@ class PluginManifest(_StrictManifestModel):
|
|||||||
if not value:
|
if not value:
|
||||||
raise ValueError("不能为空")
|
raise ValueError("不能为空")
|
||||||
if info.field_name == "id" and not _PLUGIN_ID_PATTERN.fullmatch(value):
|
if info.field_name == "id" and not _PLUGIN_ID_PATTERN.fullmatch(value):
|
||||||
raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
|
raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator("capabilities")
|
@field_validator("capabilities")
|
||||||
|
|||||||
@@ -185,6 +185,16 @@ def _setup_static_files(app: FastAPI):
|
|||||||
logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
|
logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@app.get("/maibot_statistics.html", include_in_schema=False)
|
||||||
|
async def serve_statistics_report():
|
||||||
|
report_path = (_get_project_root() / "maibot_statistics.html").resolve()
|
||||||
|
if not report_path.exists() or not report_path.is_file():
|
||||||
|
raise HTTPException(status_code=404, detail=t("core.not_found"))
|
||||||
|
|
||||||
|
response = FileResponse(report_path, media_type="text/html")
|
||||||
|
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"
|
||||||
|
return response
|
||||||
|
|
||||||
@app.get("/{full_path:path}", include_in_schema=False)
|
@app.get("/{full_path:path}", include_in_schema=False)
|
||||||
async def serve_spa(full_path: str):
|
async def serve_spa(full_path: str):
|
||||||
if not full_path or full_path == "/":
|
if not full_path or full_path == "/":
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ class ConfigSchemaGenerator:
|
|||||||
for field_name, field_info in config_class.model_fields.items():
|
for field_name, field_info in config_class.model_fields.items():
|
||||||
if field_name in {"field_docs", "_validate_any", "suppress_any_warning"}:
|
if field_name in {"field_docs", "_validate_any", "suppress_any_warning"}:
|
||||||
continue
|
continue
|
||||||
if cls._is_advanced_field(field_info):
|
|
||||||
continue
|
|
||||||
|
|
||||||
field_schema = cls._build_field_schema(config_class, field_name, field_info.annotation, field_info)
|
field_schema = cls._build_field_schema(config_class, field_name, field_info.annotation, field_info)
|
||||||
fields.append(field_schema)
|
fields.append(field_schema)
|
||||||
@@ -50,13 +48,6 @@ class ConfigSchemaGenerator:
|
|||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_advanced_field(field_info: Any) -> bool:
|
|
||||||
extra = getattr(field_info, "json_schema_extra", None)
|
|
||||||
if not isinstance(extra, dict):
|
|
||||||
return False
|
|
||||||
return extra.get("advanced", False) is True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_nested_schema(cls, annotation: Any) -> Dict[str, Any] | None:
|
def _build_nested_schema(cls, annotation: Any) -> Dict[str, Any] | None:
|
||||||
origin = get_origin(annotation)
|
origin = get_origin(annotation)
|
||||||
|
|||||||
Reference in New Issue
Block a user