-
-
- {renderSectionIcon(nestedSchema.uiIcon)}
-
{sectionTitle}
+
+
+
+
+
{sectionTitle}
+
+ {sectionDescription && (
+
+ {sectionDescription}
+
+ )}
- {sectionDescription && (
-
- {sectionDescription}
-
- )}
= ({
return (
- {/* Label with icon */}
-
+
+ {/* Label with icon */}
+
+
+ {/* Description */}
+ {schema.description && (
+
{schema.description}
+ )}
+
{/* Input component */}
{renderInputComponent()}
-
- {/* Description */}
- {schema.description && (
-
{schema.description}
- )}
)
}
diff --git a/dashboard/src/components/layout/constants.ts b/dashboard/src/components/layout/constants.ts
index 7de21c3d..f908fd0f 100644
--- a/dashboard/src/components/layout/constants.ts
+++ b/dashboard/src/components/layout/constants.ts
@@ -1,4 +1,4 @@
-import { Activity, Boxes, Database, FileSearch, FileText, Hash, Home, 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'
@@ -15,7 +15,6 @@ export const menuSections: MenuSection[] = [
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot', searchDescription: 'search.items.botConfigDesc' },
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', searchDescription: 'search.items.modelProviderDesc', tourId: 'sidebar-model-provider' },
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', searchDescription: 'search.items.modelDesc', tourId: 'sidebar-model-management' },
- { icon: Sliders, label: 'sidebar.menu.adapterConfig', path: '/config/adapter' },
],
},
{
@@ -33,7 +32,6 @@ export const menuSections: MenuSection[] = [
title: 'sidebar.groups.extensionsMonitor',
items: [
{ 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: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
diff --git a/dashboard/src/i18n/locales/en.json b/dashboard/src/i18n/locales/en.json
index a3bc03cf..3a7c43a9 100644
--- a/dashboard/src/i18n/locales/en.json
+++ b/dashboard/src/i18n/locales/en.json
@@ -500,17 +500,13 @@
"title": "Personality",
"description": "Define the bot's personality and speaking style"
},
- "emoji": {
- "title": "Emoji",
- "description": "Configure emoji-related settings"
- },
- "other": {
- "title": "Other Settings",
- "description": "Configure global slang and other basic options"
- },
- "siliconFlow": {
+ "apiProvider": {
"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": {
@@ -528,7 +524,12 @@
"selectPlatform": "Please select a platform",
"enterNickname": "Please enter a nickname",
"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": {
"loadFailedTitle": "Failed to load configuration",
@@ -667,33 +668,43 @@
"description": "Allow the bot to learn and use group-specific slang"
}
},
- "siliconFlow": {
- "about": {
- "title": "About SiliconFlow",
- "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.",
- "link": "Get an API key from SiliconFlow"
+ "apiProvider": {
+ "providerName": {
+ "label": "API Provider Name *",
+ "placeholder": "For example OpenAI, DeepSeek, or self-hosted",
+ "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": {
- "label": "SiliconFlow API Key *",
- "description": "Enter your SiliconFlow API key. Once provided, MaiBot will automatically configure all required models.",
+ "label": "API Key *",
+ "description": "Enter the API key for this provider",
"show": "Show API key",
"hide": "Hide API key"
- },
- "autoConfig": {
- "title": "The following models will be configured automatically:",
- "items": {
- "deepseek": "DeepSeek V3 - primary chat and tool model",
- "qwen3": "Qwen3 30B - frequent small tasks and tool calls",
- "qwen3Vl": "Qwen3 VL 30B - image recognition",
- "senseVoice": "SenseVoice - speech recognition",
- "bgeM3": "BGE-M3 - text embeddings",
- "lpmm": "Knowledge-base-related models (LPMM)"
+ }
+ },
+ "modelSetup": {
+ "planner": {
+ "identifier": {
+ "label": "planner Model Identifier *",
+ "description": "The real model ID provided by the API service; the model name will be initialized from it"
+ },
+ "visual": {
+ "label": "Enable vision"
}
},
- "hint": {
- "title": "Tip: ",
- "description": "After finishing the wizard, you can add more API providers and models in \"System Settings -> Model Config\"."
- }
+ "replyer": {
+ "identifier": {
+ "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."
}
}
},
diff --git a/dashboard/src/i18n/locales/ja.json b/dashboard/src/i18n/locales/ja.json
index 3dd08798..c011e655 100644
--- a/dashboard/src/i18n/locales/ja.json
+++ b/dashboard/src/i18n/locales/ja.json
@@ -500,17 +500,13 @@
"title": "人格設定",
"description": "ボットの性格や話し方を定義します"
},
- "emoji": {
- "title": "絵文字パック",
- "description": "絵文字パック関連の設定を行います"
- },
- "other": {
- "title": "その他の設定",
- "description": "グローバルスラングなどの基本オプションを設定します"
- },
- "siliconFlow": {
+ "apiProvider": {
"title": "API設定",
- "description": "SiliconFlow API キーを設定します"
+ "description": "APIプロバイダーを設定します"
+ },
+ "modelSetup": {
+ "title": "モデル設定",
+ "description": "planner と replyer モデルを設定します"
}
},
"loading": {
@@ -528,7 +524,12 @@
"selectPlatform": "プラットフォームを選択してください",
"enterNickname": "ニックネームを入力してください",
"enterQqAccount": "QQ アカウントを入力してください",
- "enterAccountId": "アカウント ID を入力してください"
+ "enterAccountId": "アカウント ID を入力してください",
+ "enterProviderName": "APIプロバイダー名を入力してください",
+ "enterBaseUrl": "API Base URL を入力してください",
+ "enterApiKey": "API Key を入力してください",
+ "enterPlannerModelIdentifier": "planner モデル識別子を入力してください",
+ "enterReplyerModelIdentifier": "replyer モデル識別子を入力してください"
},
"toast": {
"loadFailedTitle": "設定の読み込みに失敗しました",
@@ -667,33 +668,43 @@
"description": "グループ内のスラングを学習して使えるようにします"
}
},
- "siliconFlow": {
- "about": {
- "title": "SiliconFlow について",
- "description": "SiliconFlow は DeepSeek V3、Qwen、ビジョンモデル、音声認識、埋め込みモデルなど幅広いモデルを提供します。API Key が1つあれば MaiBot の全機能を利用できます。",
- "link": "SiliconFlow で API Key を取得する"
+ "apiProvider": {
+ "providerName": {
+ "label": "APIプロバイダー名 *",
+ "placeholder": "例: OpenAI、DeepSeek、自ホストサービス",
+ "description": "この名前は model_config.toml に保存され、下のモデルから参照されます"
+ },
+ "baseUrl": {
+ "label": "API Base URL *",
+ "description": "OpenAI互換エンドポイントを入力してください。例: https://api.example.com/v1"
},
"apiKey": {
- "label": "SiliconFlow API Key *",
- "description": "SiliconFlow の API Key を入力してください。入力後、MaiBot が必要なモデルを自動設定します。",
+ "label": "API Key *",
+ "description": "このプロバイダーの API Key を入力してください",
"show": "API Key を表示",
- "hide": "API Key を隠す"
- },
- "autoConfig": {
- "title": "以下のモデルが自動設定されます:",
- "items": {
- "deepseek": "DeepSeek V3 - メインの会話・ツールモデル",
- "qwen3": "Qwen3 30B - 頻繁な小タスクとツール呼び出し",
- "qwen3Vl": "Qwen3 VL 30B - 画像認識",
- "senseVoice": "SenseVoice - 音声認識",
- "bgeM3": "BGE-M3 - テキスト埋め込み",
- "lpmm": "知識ベース関連モデル (LPMM)"
+ "hide": "API Key を非表示"
+ }
+ },
+ "modelSetup": {
+ "planner": {
+ "identifier": {
+ "label": "planner モデル識別子 *",
+ "description": "APIサービスが提供する実際のモデルID。モデル名はこの識別子で初期化されます"
+ },
+ "visual": {
+ "label": "ビジョンを有効化"
}
},
- "hint": {
- "title": "ヒント:",
- "description": "ウィザード完了後は、「システム設定 -> モデル設定」でさらに API プロバイダーやモデルを追加できます。"
- }
+ "replyer": {
+ "identifier": {
+ "label": "replyer モデル識別子 *",
+ "description": "APIサービスが提供する実際のモデルID。モデル名はこの識別子で初期化されます"
+ },
+ "visual": {
+ "label": "ビジョンを有効化"
+ }
+ },
+ "saveHint": "より詳細なタスク割り当ては後で設定できます。"
}
}
},
diff --git a/dashboard/src/i18n/locales/ko.json b/dashboard/src/i18n/locales/ko.json
index 5a6f294d..7bb1487a 100644
--- a/dashboard/src/i18n/locales/ko.json
+++ b/dashboard/src/i18n/locales/ko.json
@@ -500,17 +500,13 @@
"title": "성격 설정",
"description": "봇의 성격과 말투를 정의합니다"
},
- "emoji": {
- "title": "이모지 팩",
- "description": "이모지 관련 설정을 구성합니다"
- },
- "other": {
- "title": "기타 설정",
- "description": "전역 슬랭 등 기본 옵션을 설정합니다"
- },
- "siliconFlow": {
+ "apiProvider": {
"title": "API 설정",
- "description": "SiliconFlow API 키를 설정합니다"
+ "description": "API 제공자를 설정합니다"
+ },
+ "modelSetup": {
+ "title": "모델 설정",
+ "description": "planner와 replyer 모델을 설정합니다"
}
},
"loading": {
@@ -528,7 +524,12 @@
"selectPlatform": "플랫폼을 선택해 주세요",
"enterNickname": "닉네임을 입력해 주세요",
"enterQqAccount": "QQ 계정을 입력해 주세요",
- "enterAccountId": "계정 ID를 입력해 주세요"
+ "enterAccountId": "계정 ID를 입력해 주세요",
+ "enterProviderName": "API 제공자 이름을 입력해 주세요",
+ "enterBaseUrl": "API Base URL을 입력해 주세요",
+ "enterApiKey": "API Key를 입력해 주세요",
+ "enterPlannerModelIdentifier": "planner 모델 식별자를 입력해 주세요",
+ "enterReplyerModelIdentifier": "replyer 모델 식별자를 입력해 주세요"
},
"toast": {
"loadFailedTitle": "설정 불러오기에 실패했습니다",
@@ -667,33 +668,43 @@
"description": "봇이 그룹 슬랭을 학습하고 사용할 수 있게 합니다"
}
},
- "siliconFlow": {
- "about": {
- "title": "SiliconFlow 소개",
- "description": "SiliconFlow 는 DeepSeek V3, Qwen, 비전 모델, 음성 인식, 임베딩 모델 등 폭넓은 모델을 제공합니다. API Key 하나로 MaiBot 의 모든 기능을 사용할 수 있습니다.",
- "link": "SiliconFlow 에서 API Key 받기"
+ "apiProvider": {
+ "providerName": {
+ "label": "API 제공자 이름 *",
+ "placeholder": "예: OpenAI, DeepSeek, 자체 호스팅",
+ "description": "이 이름은 model_config.toml에 저장되며 아래 모델에서 참조됩니다"
+ },
+ "baseUrl": {
+ "label": "API Base URL *",
+ "description": "OpenAI 호환 엔드포인트를 입력해 주세요. 예: https://api.example.com/v1"
},
"apiKey": {
- "label": "SiliconFlow API Key *",
- "description": "SiliconFlow API Key를 입력해 주세요. 입력하면 MaiBot 이 필요한 모델을 자동으로 구성합니다.",
+ "label": "API Key *",
+ "description": "이 제공자의 API Key를 입력해 주세요",
"show": "API Key 표시",
"hide": "API Key 숨기기"
- },
- "autoConfig": {
- "title": "다음 모델이 자동으로 구성됩니다:",
- "items": {
- "deepseek": "DeepSeek V3 - 주요 대화 및 도구 모델",
- "qwen3": "Qwen3 30B - 잦은 소규모 작업과 도구 호출",
- "qwen3Vl": "Qwen3 VL 30B - 이미지 인식",
- "senseVoice": "SenseVoice - 음성 인식",
- "bgeM3": "BGE-M3 - 텍스트 임베딩",
- "lpmm": "지식 베이스 관련 모델 (LPMM)"
+ }
+ },
+ "modelSetup": {
+ "planner": {
+ "identifier": {
+ "label": "planner 모델 식별자 *",
+ "description": "API 서비스가 제공하는 실제 모델 ID입니다. 모델 이름은 이 식별자로 초기화됩니다"
+ },
+ "visual": {
+ "label": "비전 사용"
}
},
- "hint": {
- "title": "팁: ",
- "description": "마법사를 마친 뒤에는 \"시스템 설정 -> 모델 설정\"에서 더 많은 API 제공자와 모델을 추가할 수 있습니다."
- }
+ "replyer": {
+ "identifier": {
+ "label": "replyer 모델 식별자 *",
+ "description": "API 서비스가 제공하는 실제 모델 ID입니다. 모델 이름은 이 식별자로 초기화됩니다"
+ },
+ "visual": {
+ "label": "비전 사용"
+ }
+ },
+ "saveHint": "더 자세한 작업 할당은 나중에 설정할 수 있습니다."
}
}
},
diff --git a/dashboard/src/i18n/locales/zh.json b/dashboard/src/i18n/locales/zh.json
index 4325f589..d15fc01a 100644
--- a/dashboard/src/i18n/locales/zh.json
+++ b/dashboard/src/i18n/locales/zh.json
@@ -500,17 +500,13 @@
"title": "人格配置",
"description": "定义机器人的性格和说话风格"
},
- "emoji": {
- "title": "表情包",
- "description": "配置表情包相关设置"
- },
- "other": {
- "title": "其他设置",
- "description": "配置全局黑话等基础选项"
- },
- "siliconFlow": {
+ "apiProvider": {
"title": "API配置",
- "description": "配置硅基流动 API 密钥"
+ "description": "配置 API 提供商"
+ },
+ "modelSetup": {
+ "title": "模型配置",
+ "description": "配置 planner 和 replyer 模型"
}
},
"loading": {
@@ -528,7 +524,12 @@
"selectPlatform": "请选择平台",
"enterNickname": "请输入昵称",
"enterQqAccount": "请输入 QQ 账号",
- "enterAccountId": "请输入账号 ID"
+ "enterAccountId": "请输入账号 ID",
+ "enterProviderName": "请输入 API 提供商名称",
+ "enterBaseUrl": "请输入 API Base URL",
+ "enterApiKey": "请输入 API Key",
+ "enterPlannerModelIdentifier": "请输入 planner 模型标识符",
+ "enterReplyerModelIdentifier": "请输入 replyer 模型标识符"
},
"toast": {
"loadFailedTitle": "加载配置失败",
@@ -559,8 +560,8 @@
"footer": "您可以随时在设置中修改这些配置",
"defaults": {
"personality": {
- "personality": "是一个女大学生,现在在读大二,会刷贴吧。",
- "replyStyle": "请回复得平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧、知乎和微博的回复风格。",
+ "personality": "是一个大二女大学生,现在正在上网和群友聊天。有轻微攻击性",
+ "replyStyle": "你的风格平淡简短。可以参考贴吧,知乎和微博的回复风格。不浮夸不长篇大论,不要过分修辞和复杂句。",
"multipleReplyStyles": {
"plain": "你的风格平淡但不失讽刺,很简短,很白话。可以参考贴吧、微博的回复风格。",
"shortText": "用1-2个字进行回复",
@@ -577,7 +578,7 @@
"platform": {
"label": "平台 *",
"placeholder": "请选择平台",
- "description": "选择机器人运行的平台",
+ "description": "选择麦麦Bot运行的平台",
"options": {
"custom": "其他平台"
}
@@ -589,7 +590,7 @@
"qqAccount": {
"label": "QQ账号 *",
"placeholder": "请输入机器人的 QQ 账号",
- "description": "机器人登录使用的 QQ 账号"
+ "description": "运行麦麦Bot的 QQ 账号"
},
"primaryAccount": {
"label": "账号 ID *",
@@ -599,7 +600,7 @@
"nickname": {
"label": "昵称 *",
"placeholder": "请输入机器人的昵称",
- "description": "机器人的主要称呼名称"
+ "description": "麦麦Bot的名称"
},
"alias": {
"label": "别名",
@@ -667,33 +668,43 @@
"description": "允许机器人学习和使用群组黑话"
}
},
- "siliconFlow": {
- "about": {
- "title": "关于硅基流动 (SiliconFlow)",
- "description": "硅基流动提供了完整的模型覆盖,包括 DeepSeek V3、Qwen、视觉模型、语音识别和嵌入模型。只需一个 API Key 即可使用麦麦的所有功能!",
- "link": "前往硅基流动获取 API Key"
+ "apiProvider": {
+ "providerName": {
+ "label": "API 提供商名称 *",
+ "placeholder": "例如 OpenAI、DeepSeek、自建服务",
+ "description": "为api提供商命名"
+ },
+ "baseUrl": {
+ "label": "API Base URL *",
+ "description": "请填写 OpenAI 兼容接口地址,例如 https://api.example.com/v1"
},
"apiKey": {
- "label": "SiliconFlow API Key *",
- "description": "请输入您的硅基流动 API 密钥。获取后,麦麦将自动配置所有必需的模型。",
+ "label": "API Key *",
+ "description": "请填写该提供商的 API Key",
"show": "显示 API Key",
"hide": "隐藏 API Key"
- },
- "autoConfig": {
- "title": "将自动配置以下模型:",
- "items": {
- "deepseek": "DeepSeek V3 - 主要对话和工具模型",
- "qwen3": "Qwen3 30B - 高频小任务和工具调用",
- "qwen3Vl": "Qwen3 VL 30B - 图像识别",
- "senseVoice": "SenseVoice - 语音识别",
- "bgeM3": "BGE-M3 - 文本嵌入",
- "lpmm": "知识库相关模型 (LPMM)"
+ }
+ },
+ "modelSetup": {
+ "planner": {
+ "identifier": {
+ "label": "planner 模型标识符 *",
+ "description": "API 服务商提供的真实模型 ID,模型名称会自动初始化为该标识符"
+ },
+ "visual": {
+ "label": "启用视觉"
}
},
- "hint": {
- "title": "💡 提示:",
- "description": "完成向导后,您可以在“系统设置 -> 模型配置”中添加更多 API 提供商和模型。"
- }
+ "replyer": {
+ "identifier": {
+ "label": "replyer 模型标识符 *",
+ "description": "API 服务商提供的真实模型 ID,模型名称会自动初始化为该标识符"
+ },
+ "visual": {
+ "label": "启用视觉"
+ }
+ },
+ "saveHint": "你可以稍后配置更详细的任务分配。"
}
}
},
diff --git a/dashboard/src/lib/maisaka-monitor-client.ts b/dashboard/src/lib/maisaka-monitor-client.ts
index 76d3f972..065cf97a 100644
--- a/dashboard/src/lib/maisaka-monitor-client.ts
+++ b/dashboard/src/lib/maisaka-monitor-client.ts
@@ -26,6 +26,10 @@ export interface MaisakaToolCall {
export interface SessionStartEvent {
session_id: string
session_name: string
+ is_group_chat?: boolean
+ group_id?: string | null
+ user_id?: string | null
+ platform?: string
timestamp: number
}
diff --git a/dashboard/src/lib/plugin-api/marketplace.ts b/dashboard/src/lib/plugin-api/marketplace.ts
index a7054088..0842a91a 100644
--- a/dashboard/src/lib/plugin-api/marketplace.ts
+++ b/dashboard/src/lib/plugin-api/marketplace.ts
@@ -35,6 +35,12 @@ interface PluginApiResponse {
}
homepage_url?: string
repository_url?: string
+ urls?: {
+ repository?: string
+ homepage?: string
+ documentation?: string
+ issues?: string
+ }
keywords: string[]
categories?: string[]
default_locale: string
@@ -44,6 +50,28 @@ interface PluginApiResponse {
[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)
*/
@@ -88,21 +116,7 @@ export async function fetchPluginList(): Promise> {
})
.map((item) => ({
id: item.id,
- 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,
- },
+ manifest: normalizePluginManifest(item.manifest),
downloads: 0,
rating: 0,
review_count: 0,
diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx
index 30a88c3d..86555e85 100644
--- a/dashboard/src/routes/index.tsx
+++ b/dashboard/src/routes/index.tsx
@@ -29,6 +29,7 @@ import {
} from 'recharts'
import {
Activity,
+ BarChart3,
TrendingUp,
DollarSign,
Clock,
@@ -45,6 +46,7 @@ import {
AlertCircle,
ClipboardList,
ClipboardCheck,
+ ExternalLink,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -566,6 +568,13 @@ function IndexPageContent() {
{t('home.quickActions.systemSettings')}
+
diff --git a/dashboard/src/routes/monitor/maisaka-monitor.tsx b/dashboard/src/routes/monitor/maisaka-monitor.tsx
index 8591c3eb..84acfd24 100644
--- a/dashboard/src/routes/monitor/maisaka-monitor.tsx
+++ b/dashboard/src/routes/monitor/maisaka-monitor.tsx
@@ -104,10 +104,17 @@ function SessionSidebar({
)}
>
-
- {session.sessionName}
-
-
+
+ {session.isGroupChat !== undefined && (
+
+ {session.isGroupChat ? '群' : '私'}
+
+ )}
+
+ {session.sessionName}
+
+
+
{session.eventCount}
diff --git a/dashboard/src/routes/monitor/use-maisaka-monitor.ts b/dashboard/src/routes/monitor/use-maisaka-monitor.ts
index 3ccf705d..bba94207 100644
--- a/dashboard/src/routes/monitor/use-maisaka-monitor.ts
+++ b/dashboard/src/routes/monitor/use-maisaka-monitor.ts
@@ -26,6 +26,10 @@ export interface TimelineEntry {
export interface SessionInfo {
sessionId: string
sessionName: string
+ isGroupChat?: boolean
+ groupId?: string | null
+ userId?: string | null
+ platform?: string
lastActivity: number
eventCount: number
}
@@ -33,18 +37,62 @@ export interface SessionInfo {
/** 最大保留的时间线条目数 */
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 cachedTimeline: TimelineEntry[] = []
+let cachedSessions: Map
= new Map()
+let cachedSelectedSession: string | null = null
export function useMaisakaMonitor() {
- const [timeline, setTimeline] = useState([])
- const [sessions, setSessions] = useState
)
}
-// ====== 步骤3:表情包配置 ======
-interface EmojiFormProps {
- config: EmojiConfig
- onChange: (config: EmojiConfig) => void
+// ====== 步骤3:API 提供商配置 ======
+interface ApiProviderSetupFormProps {
+ config: ApiProviderSetupConfig
+ onChange: (config: ApiProviderSetupConfig) => void
}
-export function EmojiForm({ config, onChange }: EmojiFormProps) {
- const { t } = useTranslation()
-
- return (
-
-
-
-
onChange({ ...config, emoji_send_num: Number(e.target.value) })}
- />
-
- {t('setupPage.forms.emoji.emojiSendNum.description')}
-
-
-
-
-
-
onChange({ ...config, max_reg_num: Number(e.target.value) })}
- />
-
- {t('setupPage.forms.emoji.maxRegNum.description')}
-
-
-
-
-
-
-
- {t('setupPage.forms.emoji.doReplace.description')}
-
-
-
onChange({ ...config, do_replace: checked })}
- />
-
-
-
-
-
onChange({ ...config, check_interval: Number(e.target.value) })}
- />
-
- {t('setupPage.forms.emoji.checkInterval.description')}
-
-
-
-
-
-
-
-
-
- {t('setupPage.forms.emoji.stealEmoji.description')}
-
-
-
onChange({ ...config, steal_emoji: checked })}
- />
-
-
-
-
-
-
- {t('setupPage.forms.emoji.contentFiltration.description')}
-
-
-
onChange({ ...config, content_filtration: checked })}
- />
-
-
- {config.content_filtration && (
-
-
-
onChange({ ...config, filtration_prompt: e.target.value })}
- />
-
- {t('setupPage.forms.emoji.filtrationPrompt.description')}
-
-
- )}
-
- )
-}
-
-// ====== 步骤4:其他基础配置 ======
-interface OtherBasicFormProps {
- config: OtherBasicConfig
- onChange: (config: OtherBasicConfig) => void
-}
-
-export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
- const { t } = useTranslation()
-
- return (
-
-
-
-
-
- {t('setupPage.forms.other.allGlobal.description')}
-
-
-
onChange({ ...config, all_global: checked })}
- />
-
-
- )
-}
-
-// ====== 步骤5:硅基流动API配置 ======
-interface SiliconFlowFormProps {
- config: SiliconFlowConfig
- onChange: (config: SiliconFlowConfig) => void
-}
-
-export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
+export function ApiProviderSetupForm({ config, onChange }: ApiProviderSetupFormProps) {
const { t } = useTranslation()
const [showApiKey, setShowApiKey] = useState(false)
const apiKeyToggleLabel = showApiKey
- ? t('setupPage.forms.siliconFlow.apiKey.hide')
- : t('setupPage.forms.siliconFlow.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'),
- ]
+ ? t('setupPage.forms.apiProvider.apiKey.hide')
+ : t('setupPage.forms.apiProvider.apiKey.show')
return (
-
-
+
+
+
onChange({ ...config, provider_name: e.target.value })}
+ />
+
+ {t('setupPage.forms.apiProvider.providerName.description')}
+
-
+
+
onChange({ ...config, base_url: e.target.value })}
+ className="font-mono"
+ />
+
+ {t('setupPage.forms.apiProvider.baseUrl.description')}
+
+
+
+
+
onChange({ api_key: e.target.value })}
+ onChange={(e) => onChange({ ...config, api_key: e.target.value })}
className="pr-10 font-mono"
/>
- {t('setupPage.forms.siliconFlow.apiKey.description')}
-
-
-
-
-
{t('setupPage.forms.siliconFlow.autoConfig.title')}
-
- {autoConfigItems.map((item) => (
- - {item}
- ))}
-
-
-
-
-
- {t('setupPage.forms.siliconFlow.hint.title')}
- {t('setupPage.forms.siliconFlow.hint.description')}
+ {t('setupPage.forms.apiProvider.apiKey.description')}
)
}
+
+// ====== 步骤4:基础模型配置 ======
+interface ModelSetupFormProps {
+ config: ModelSetupConfig
+ onChange: (config: ModelSetupConfig) => void
+}
+
+export function ModelSetupForm({ config, onChange }: ModelSetupFormProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+
+ onChange({
+ ...config,
+ planner_model_identifier: e.target.value,
+ planner_model_name: e.target.value,
+ })
+ }
+ className="font-mono"
+ />
+
+ {t('setupPage.forms.modelSetup.planner.identifier.description')}
+
+
+
+
+
+
+ onChange({ ...config, planner_visual: checked })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ ...config,
+ replyer_model_identifier: e.target.value,
+ replyer_model_name: e.target.value,
+ })
+ }
+ className="font-mono"
+ />
+
+ {t('setupPage.forms.modelSetup.replyer.identifier.description')}
+
+
+
+
+
+
+ onChange({ ...config, replyer_visual: checked })
+ }
+ />
+
+
+
+
+
+ {t('setupPage.forms.modelSetup.saveHint')}
+
+
+ )
+}
diff --git a/dashboard/src/routes/setup/api.ts b/dashboard/src/routes/setup/api.ts
index 8b93ce69..5332b7fe 100644
--- a/dashboard/src/routes/setup/api.ts
+++ b/dashboard/src/routes/setup/api.ts
@@ -4,13 +4,49 @@ import { parseResponse, throwIfError } from '@/lib/api-helpers'
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
import type {
+ ApiProviderSetupConfig,
BotBasicConfig,
- EmojiConfig,
- OtherBasicConfig,
+ ModelSetupConfig,
PersonalityConfig,
- SiliconFlowConfig,
} 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
+}
+
+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
+}
+
// ===== 读取配置 =====
// 读取Bot基础配置
@@ -56,73 +92,57 @@ export async function loadPersonalityConfig(): Promise {
}
}
-// 读取表情包配置
-export async function loadEmojiConfig(): Promise {
- 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
-
- 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 {
- 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 {
+async function loadModelConfig(): Promise {
const response = await fetchWithAuth('/api/webui/config/model', {
method: 'GET',
headers: getAuthHeaders(),
})
- const result = await parseResponse<{
- config: {
- api_providers?: Array<{ name: string; api_key?: string }>
- }
- }>(response)
+ const result = await parseResponse<{ config: ModelConfig }>(response)
const data = throwIfError(result)
- const modelConfig = data.config
+ return data.config || {}
+}
- // 获取SiliconFlow提供商的API Key
- const apiProviders = modelConfig.api_providers || []
- const siliconFlowProvider = apiProviders.find((p) => p.name === 'SiliconFlow')
+// 读取 API 提供商配置
+export async function loadApiProviderSetupConfig(): Promise {
+ 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 {
- api_key: siliconFlowProvider?.api_key || '',
+ provider_name: providerName,
+ base_url: provider?.base_url || '',
+ api_key: '',
+ }
+}
+
+// 读取基础模型配置
+export async function loadModelSetupConfig(): Promise {
+ 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) {
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',
headers: getAuthHeaders(),
body: JSON.stringify(config),
@@ -165,58 +172,62 @@ export async function saveEmojiConfig(config: EmojiConfig) {
return throwIfError(result)
}
-// 保存其他基础配置(黑话)
-export async function saveOtherBasicConfig(config: OtherBasicConfig) {
- const response = await fetchWithAuth('/api/webui/config/bot/section/expression', {
- method: 'POST',
- headers: getAuthHeaders(),
- body: JSON.stringify({ all_global_jargon: config.all_global }),
- })
-
- const result = await parseResponse(response)
- return throwIfError(result)
+function createBasicModel(
+ modelName: string,
+ modelIdentifier: string,
+ providerName: string,
+ visual: boolean,
+ existing?: ModelInfo
+): ModelInfo {
+ return {
+ price_in: 0,
+ 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配置
-export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
- // 1. 读取现有配置
- const response = await fetchWithAuth('/api/webui/config/model', {
- method: 'GET',
- headers: getAuthHeaders(),
- })
+function upsertModel(models: ModelInfo[], model: ModelInfo): ModelInfo[] {
+ const index = models.findIndex((item) => item.name === model.name)
+ if (index >= 0) {
+ return models.map((item, itemIndex) => (itemIndex === index ? model : item))
+ }
+ return [...models, model]
+}
- const result = await parseResponse<{
- config: {
- api_providers?: Array>
- }
- }>(response)
- const currentModelConfig = throwIfError(result)
- const modelConfig = currentModelConfig.config
+// 保存 API 提供商配置
+export async function saveApiProviderSetupConfig(config: ApiProviderSetupConfig) {
+ const modelConfig = await loadModelConfig()
+ const providerName = config.provider_name.trim()
- // 2. 更新SiliconFlow提供商的API Key
const apiProviders = modelConfig.api_providers || []
- const siliconFlowIndex = apiProviders.findIndex((p) => p.name === 'SiliconFlow')
-
- if (siliconFlowIndex >= 0) {
- // 更新现有提供商的API Key
- apiProviders[siliconFlowIndex] = {
- ...apiProviders[siliconFlowIndex],
- api_key: config.api_key,
- }
- } else {
- // 如果不存在,创建新的SiliconFlow提供商
- apiProviders.push({
- name: 'SiliconFlow',
- base_url: 'https://api.siliconflow.cn/v1',
- api_key: config.api_key,
- client_type: 'openai',
- max_retry: 3,
- timeout: 120,
- retry_interval: 5,
- })
+ const providerIndex = apiProviders.findIndex((provider) => provider.name === providerName)
+ const providerConfig: ApiProviderConfig = {
+ name: providerName,
+ base_url: config.base_url.trim(),
+ api_key: config.api_key.trim(),
+ client_type: 'openai',
+ max_retry: 3,
+ timeout: 120,
+ retry_interval: 5,
+ }
+
+ if (providerIndex >= 0) {
+ apiProviders[providerIndex] = {
+ ...apiProviders[providerIndex],
+ ...providerConfig,
+ }
+ } else {
+ apiProviders.push(providerConfig)
}
- // 3. 保存更新后的配置
const updatedConfig = {
...modelConfig,
api_providers: apiProviders,
@@ -232,6 +243,77 @@ export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
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() {
const response = await fetchWithAuth('/api/webui/setup/complete', {
diff --git a/dashboard/src/routes/setup/index.tsx b/dashboard/src/routes/setup/index.tsx
index fa943cb7..787d6854 100644
--- a/dashboard/src/routes/setup/index.tsx
+++ b/dashboard/src/routes/setup/index.tsx
@@ -1,13 +1,12 @@
import { useNavigate } from '@tanstack/react-router'
import {
ArrowRight,
+ Brain,
Bot,
CheckCircle2,
Globe,
Key,
- Settings,
SkipForward,
- Smile,
Sparkles,
User,
} from 'lucide-react'
@@ -38,31 +37,27 @@ import { cn } from '@/lib/utils'
import { APP_NAME } from '@/lib/version'
import { useToast } from '@/hooks/use-toast'
import type {
+ ApiProviderSetupConfig,
SetupStep,
BotBasicConfig,
+ ModelSetupConfig,
PersonalityConfig,
- EmojiConfig,
- OtherBasicConfig,
- SiliconFlowConfig,
} from './types'
import {
+ ApiProviderSetupForm,
BotBasicForm,
+ ModelSetupForm,
PersonalityForm,
- EmojiForm,
- OtherBasicForm,
- SiliconFlowForm,
} from './StepForms'
import {
loadBotBasicConfig,
loadPersonalityConfig,
- loadEmojiConfig,
- loadOtherBasicConfig,
- loadSiliconFlowConfig,
+ loadApiProviderSetupConfig,
+ loadModelSetupConfig,
saveBotBasicConfig,
savePersonalityConfig,
- saveEmojiConfig,
- saveOtherBasicConfig,
- saveSiliconFlowConfig,
+ saveApiProviderSetupConfig,
+ saveModelSetupConfig,
completeSetup,
} from './api'
import { RestartProvider, useRestart } from '@/lib/restart-context'
@@ -103,15 +98,6 @@ function SetupPageContent() {
],
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 [isCompleting, setIsCompleting] = useState(false)
const [isSaving, setIsSaving] = useState(false)
@@ -131,17 +117,21 @@ function SetupPageContent() {
createDefaultPersonalityConfig()
)
- // 步骤3:表情包配置
- const [emoji, setEmoji] = useState(() => createDefaultEmojiConfig())
-
- // 步骤4:其他基础配置
- const [otherBasic, setOtherBasic] = useState({
- all_global: true,
+ // 步骤3:API 提供商配置
+ const [apiProviderSetup, setApiProviderSetup] = useState({
+ provider_name: '',
+ base_url: '',
+ api_key: '',
})
- // 步骤5:硅基流动API配置
- const [siliconFlow, setSiliconFlow] = useState({
- api_key: '',
+ // 步骤4:基础模型配置
+ const [modelSetup, setModelSetup] = useState({
+ planner_model_name: '',
+ planner_model_identifier: '',
+ planner_visual: false,
+ replyer_model_name: '',
+ replyer_model_identifier: '',
+ replyer_visual: false,
})
const steps: SetupStep[] = [
@@ -158,23 +148,17 @@ function SetupPageContent() {
icon: User,
},
{
- id: 'emoji',
- title: t('setupPage.steps.emoji.title'),
- description: t('setupPage.steps.emoji.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'),
+ id: 'api-provider',
+ title: t('setupPage.steps.apiProvider.title'),
+ description: t('setupPage.steps.apiProvider.description'),
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
@@ -186,19 +170,17 @@ function SetupPageContent() {
setIsLoading(true)
// 并行加载所有配置
- const [bot, personality, emoji, other, silicon] = await Promise.all([
+ const [bot, personality, apiProvider, model] = await Promise.all([
loadBotBasicConfig(),
loadPersonalityConfig(),
- loadEmojiConfig(),
- loadOtherBasicConfig(),
- loadSiliconFlowConfig(),
+ loadApiProviderSetupConfig(),
+ loadModelSetupConfig(),
])
setBotBasic(bot)
setPersonality(personality)
- setEmoji(emoji)
- setOtherBasic(other)
- setSiliconFlow(silicon)
+ setApiProviderSetup(apiProvider)
+ setModelSetup(model)
} catch (error) {
toast({
title: t('setupPage.toast.loadFailedTitle'),
@@ -225,14 +207,11 @@ function SetupPageContent() {
case 1: // 人格配置
await savePersonalityConfig(personality)
break
- case 2: // 表情包
- await saveEmojiConfig(emoji)
+ case 2: // API 提供商
+ await saveApiProviderSetupConfig(apiProviderSetup)
break
- case 3: // 其他设置
- await saveOtherBasicConfig(otherBasic)
- break
- case 4: // 硅基流动API
- await saveSiliconFlowConfig(siliconFlow)
+ case 3: // 基础模型
+ await saveModelSetupConfig(modelSetup, apiProviderSetup.provider_name)
break
}
@@ -272,6 +251,24 @@ function SetupPageContent() {
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 () => {
// Step 1 验证
if (currentStep === 0) {
@@ -285,6 +282,28 @@ function SetupPageContent() {
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()
@@ -306,7 +325,18 @@ function SetupPageContent() {
setIsCompleting(true)
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()
if (!saved) {
setIsCompleting(false)
@@ -357,11 +387,9 @@ function SetupPageContent() {
case 1:
return
case 2:
- return
+ return
case 3:
- return
- case 4:
- return
+ return
default:
return null
}
diff --git a/dashboard/src/routes/setup/types.ts b/dashboard/src/routes/setup/types.ts
index 5447f90a..91414358 100644
--- a/dashboard/src/routes/setup/types.ts
+++ b/dashboard/src/routes/setup/types.ts
@@ -24,23 +24,19 @@ export interface PersonalityConfig {
multiple_probability: number
}
-// 步骤3:表情包配置
-export interface EmojiConfig {
- emoji_send_num: number
- max_reg_num: number
- 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 {
+// 步骤3:API 提供商配置
+export interface ApiProviderSetupConfig {
+ provider_name: string
+ base_url: 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
+}
diff --git a/dashboard/src/types/plugin.ts b/dashboard/src/types/plugin.ts
index 51fac848..ddbcecaf 100644
--- a/dashboard/src/types/plugin.ts
+++ b/dashboard/src/types/plugin.ts
@@ -36,6 +36,13 @@ export interface PluginManifest {
homepage_url?: string
/** 插件仓库地址(可选) */
repository_url?: string
+ /** Manifest v2 URL 集合(可选) */
+ urls?: {
+ repository?: string
+ homepage?: string
+ documentation?: string
+ issues?: string
+ }
/** 插件关键词 */
keywords: string[]
/** 插件分类(可选) */
diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts
index 11c80b04..46c2bbd7 100644
--- a/dashboard/vite.config.ts
+++ b/dashboard/vite.config.ts
@@ -17,6 +17,10 @@ export default defineConfig({
cookieDomainRewrite: '', // 移除域名限制
cookiePathRewrite: '/', // 确保路径一致
},
+ '/maibot_statistics.html': {
+ target: 'http://127.0.0.1:8001',
+ changeOrigin: true,
+ },
},
},
resolve: {
diff --git a/pyproject.toml b/pyproject.toml
index a63e074a..89d25030 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ dependencies = [
"jieba>=0.42.1",
"json-repair>=0.47.6",
"maim-message>=0.6.2",
- "maibot-dashboard==1.0.1.dev2026050251",
+ "maibot-dashboard>=1.0.2.dev2026050359",
"maibot-plugin-sdk>=2.4.0",
"matplotlib>=3.10.5",
"mcp",
diff --git a/pytests/test_plugin_runtime.py b/pytests/test_plugin_runtime.py
index 095002e2..840f2dcf 100644
--- a/pytests/test_plugin_runtime.py
+++ b/pytests/test_plugin_runtime.py
@@ -995,6 +995,14 @@ class TestManifestValidator:
assert len(validator.errors) == 0
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):
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
diff --git a/src/config/config.py b/src/config/config.py
index fbb79182..6cf3ac08 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -58,7 +58,7 @@ MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute(
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
MMC_VERSION: str = "1.0.0"
-CONFIG_VERSION: str = "8.9.21"
+CONFIG_VERSION: str = "8.10.1"
MODEL_CONFIG_VERSION: str = "1.14.6"
logger = get_logger("config")
diff --git a/src/config/official_configs.py b/src/config/official_configs.py
index 5d08d57f..2f22e453 100644
--- a/src/config/official_configs.py
+++ b/src/config/official_configs.py
@@ -63,6 +63,7 @@ class BotConfig(ConfigBase):
json_schema_extra={
"x-widget": "custom",
"x-icon": "tags",
+ "advanced": True,
},
)
"""别名列表"""
@@ -472,6 +473,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "message-circle-warning",
+ "advanced": True,
},
)
"""是否启用反馈驱动的延迟记忆纠错任务"""
@@ -482,6 +484,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "clock-4",
+ "advanced": True,
},
)
"""反馈窗口时长(小时),以 query_memory 执行时间为起点"""
@@ -492,6 +495,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "timer",
+ "advanced": True,
},
)
"""反馈纠错定时任务轮询间隔(分钟)"""
@@ -503,6 +507,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "list-ordered",
+ "advanced": True,
},
)
"""反馈纠错每轮最大处理任务数"""
@@ -515,6 +520,7 @@ class MemoryConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "gauge",
"step": 0.01,
+ "advanced": True,
},
)
"""自动应用纠错动作的最低置信度阈值"""
@@ -526,6 +532,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "messages-square",
+ "advanced": True,
},
)
"""每个纠错任务最多使用的窗口内用户反馈消息数"""
@@ -535,6 +542,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "filter",
+ "advanced": True,
},
)
"""是否启用纠错前置预筛(用于减少不必要的模型调用)"""
@@ -544,6 +552,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "sticky-note",
+ "advanced": True,
},
)
"""是否为受影响 paragraph 写入已纠正旧事实标记"""
@@ -553,6 +562,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "eye-off",
+ "advanced": True,
},
)
"""是否在用户侧查询中硬过滤带有 stale 标记的 paragraph"""
@@ -562,6 +572,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "user-round-search",
+ "advanced": True,
},
)
"""是否在反馈纠错后将受影响人物画像加入刷新队列"""
@@ -571,6 +582,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "refresh-ccw",
+ "advanced": True,
},
)
"""人物画像处于脏队列时,读取是否强制刷新而不直接复用旧快照"""
@@ -580,6 +592,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "clapperboard",
+ "advanced": True,
},
)
"""是否在反馈纠错后将受影响 source 加入 episode 重建队列"""
@@ -589,6 +602,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "ban",
+ "advanced": True,
},
)
"""episode source 处于重建队列时,是否对用户侧查询做屏蔽"""
@@ -599,6 +613,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "repeat",
+ "advanced": True,
},
)
"""反馈纠错二阶段一致性后台协调任务轮询间隔(分钟)"""
@@ -610,6 +625,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "list-restart",
+ "advanced": True,
},
)
"""反馈纠错二阶段一致性每轮处理 profile/episode 队列的批大小"""
@@ -1350,6 +1366,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.01,
+ "advanced": True,
},
)
"""单字替换概率"""
@@ -1359,6 +1376,7 @@ class ChineseTypoConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "hash",
+ "advanced": True,
},
)
"""最小字频阈值"""
@@ -1371,6 +1389,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.1,
+ "advanced": True,
},
)
"""声调错误概率"""
@@ -1383,6 +1402,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.001,
+ "advanced": True,
},
)
"""整词替换概率"""
diff --git a/src/maisaka/monitor_events.py b/src/maisaka/monitor_events.py
index ffe85e11..d637c19a 100644
--- a/src/maisaka/monitor_events.py
+++ b/src/maisaka/monitor_events.py
@@ -143,6 +143,33 @@ def _serialize_messages(messages: List[Any]) -> List[Dict[str, Any]]:
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]]:
"""标准化最终 planner 卡中的工具结果列表。"""
@@ -266,6 +293,7 @@ async def _broadcast(event: str, data: Dict[str, Any]) -> None:
try:
from src.webui.routers.websocket.manager import websocket_manager
+ data = _enrich_session_identity(data)
subscription_key = f"{MONITOR_DOMAIN}:{MONITOR_TOPIC}"
total_connections = len(websocket_manager.connections)
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)
-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", {
"session_id": session_id,
"session_name": session_name,
+ "is_group_chat": is_group_chat,
+ "group_id": group_id,
+ "user_id": user_id,
+ "platform": platform,
"timestamp": time.time(),
})
diff --git a/src/maisaka/runtime.py b/src/maisaka/runtime.py
index 88c6e4a0..0d48e6bb 100644
--- a/src/maisaka/runtime.py
+++ b/src/maisaka/runtime.py
@@ -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.stage_status_board import remove_stage_status, update_stage_status
from .history_utils import drop_leading_orphan_tool_results
+from .monitor_events import emit_session_start
from .reasoning_engine import MaisakaReasoningEngine
from .reply_effect import ReplyEffectTracker
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._reasoning_engine = MaisakaReasoningEngine(self)
+ self._monitor_session_start_task: Optional[asyncio.Task[None]] = None
self._tool_registry = ToolRegistry()
self._reply_effect_tracker = ReplyEffectTracker(
session_id=self.session_id,
@@ -144,6 +146,24 @@ class MaisakaHeartFlowChatting:
judge_runner=self._run_reply_effect_judge,
)
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
def _is_reply_effect_tracking_enabled() -> bool:
diff --git a/src/plugin_runtime/runner/manifest_validator.py b/src/plugin_runtime/runner/manifest_validator.py
index 92f0b315..de830fb9 100644
--- a/src/plugin_runtime/runner/manifest_validator.py
+++ b/src/plugin_runtime/runner/manifest_validator.py
@@ -24,7 +24,7 @@ from src.common.logger import get_logger
logger = get_logger("plugin_runtime.runner.manifest_validator")
_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._-]*$")
_HTTP_URL_PATTERN = re.compile(r"^https?://.+$")
@@ -379,7 +379,7 @@ class PluginDependencyDefinition(_StrictManifestModel):
ValueError: 当 ID 不符合规则时抛出。
"""
if not _PLUGIN_ID_PATTERN.fullmatch(value):
- raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
+ raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
return value
@field_validator("version_spec")
@@ -548,7 +548,7 @@ class PluginManifest(_StrictManifestModel):
if not value:
raise ValueError("不能为空")
if info.field_name == "id" and not _PLUGIN_ID_PATTERN.fullmatch(value):
- raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
+ raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
return value
@field_validator("capabilities")
diff --git a/src/webui/app.py b/src/webui/app.py
index e7ccbe5f..6a0d5cf2 100644
--- a/src/webui/app.py
+++ b/src/webui/app.py
@@ -185,6 +185,16 @@ def _setup_static_files(app: FastAPI):
logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
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)
async def serve_spa(full_path: str):
if not full_path or full_path == "/":
diff --git a/src/webui/config_schema.py b/src/webui/config_schema.py
index 5d50a42d..9658ec9c 100644
--- a/src/webui/config_schema.py
+++ b/src/webui/config_schema.py
@@ -19,8 +19,6 @@ class ConfigSchemaGenerator:
for field_name, field_info in config_class.model_fields.items():
if field_name in {"field_docs", "_validate_any", "suppress_any_warning"}:
continue
- if cls._is_advanced_field(field_info):
- continue
field_schema = cls._build_field_schema(config_class, field_name, field_info.annotation, field_info)
fields.append(field_schema)
@@ -50,13 +48,6 @@ class ConfigSchemaGenerator:
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
def _build_nested_schema(cls, annotation: Any) -> Dict[str, Any] | None:
origin = get_origin(annotation)