Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
DawnARC
2026-05-02 21:46:04 +08:00
31 changed files with 366 additions and 185 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "maibot-dashboard",
"private": true,
"version": "1.0.1",
"version": "1.0.2",
"type": "module",
"main": "./out/main/index.js",
"scripts": {

View File

@@ -29,7 +29,7 @@ export function Sidebar({
return (
<aside
className={cn(
'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0',
'fixed inset-y-0 left-0 z-50 isolate flex flex-col border-r transition-all duration-300 lg:relative lg:z-0 lg:h-full',
inheritsPageBackground ? 'bg-transparent' : 'bg-card',
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
'w-64 lg:w-auto',
@@ -46,9 +46,11 @@ export function Sidebar({
<ScrollArea className={cn(
'relative z-10',
"flex-1 overflow-x-hidden",
"min-h-0 flex-1 overflow-x-hidden",
!sidebarOpen && "lg:w-16"
)}>
)}
viewportClassName="[&>div]:!block"
>
<nav
aria-label={t('a11y.sidebarNav')}
className={cn(

View File

@@ -6,7 +6,9 @@ import { useTour } from './use-tour'
// Joyride 主题配置
const joyrideStyles = {
options: {
zIndex: 10000,
// 提到 portal 容器99999之上确保 overlay/spotlight/tooltip 都在最上层;
// overlay 的 z-index 由 react-joyride 内部基于 options.zIndex 推算,必须大于 floater 才能让 tooltip 按钮可点击。
zIndex: 100000,
primaryColor: 'hsl(var(--color-primary))',
textColor: 'hsl(var(--color-foreground))',
backgroundColor: 'hsl(var(--color-background))',
@@ -197,13 +199,6 @@ export function TourRenderer() {
locale={locale}
scrollOffset={80}
scrollToFirstStep
floaterProps={{
styles: {
floater: {
zIndex: 99999,
},
},
}}
/>
)

View File

@@ -54,7 +54,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-[min(calc(100vw-2rem),var(--dialog-width,32rem))] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-hidden border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 flex w-[min(calc(100vw-2rem),var(--dialog-width,32rem))] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col gap-4 overflow-hidden border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
onPointerDownOutside={preventOutsideClose ? (e) => e.preventDefault() : undefined}
@@ -94,13 +94,17 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(
({ className, children, allowHorizontalScroll = false, contentClassName, scrollbars, viewportClassName, ...props }, ref) => (
({ className, children, allowHorizontalScroll = false, contentClassName, scrollbars, viewportClassName, type, ...props }, ref) => (
// 关键:在 flex-col 的 DialogContent 中DialogBody 既要在内容多时撑到 max-h 上限并滚动,
// 又要在内容少时让 dialog 自然收缩。直接在 ScrollArea Root 上 flex-1 + min-h-0 即可:
// Radix Viewport 内部 wrapper 默认 display:table 会撑开自然高度,所以需要强制 block。
<ScrollArea
ref={ref as never}
className={cn("min-h-0 flex-1", className)}
className={cn("min-h-0 flex-1 flex flex-col", className)}
contentClassName={cn(allowHorizontalScroll && "min-w-full w-max", contentClassName)}
scrollbars={scrollbars ?? (allowHorizontalScroll ? "both" : "vertical")}
viewportClassName={cn("pr-4", viewportClassName)}
viewportClassName={cn("min-h-0 flex-1 pr-4 [&>div]:!block", viewportClassName)}
type={type ?? "always"}
{...props}
>
{children}

View File

@@ -19,7 +19,10 @@ const ScrollArea = React.forwardRef<
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport ref={viewportRef} className={cn("h-full w-full rounded-[inherit]", viewportClassName)}>
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
className={cn("h-full w-full rounded-[inherit]", viewportClassName)}
>
<div className={contentClassName}>{children}</div>
</ScrollAreaPrimitive.Viewport>
{scrollbars !== "horizontal" && <ScrollBar />}

View File

@@ -158,7 +158,14 @@ export async function fetchProviderModels(
endpoint,
})
const response = await fetchWithAuth(`/api/webui/models/list?${params}`)
return parseResponse<ModelListItem[]>(response)
// 后端返回 { success, models, provider, count },需要展开取出 models 数组
const parsed = await parseResponse<{ models?: ModelListItem[] } | ModelListItem[]>(response)
if (!parsed.success) {
return parsed
}
const body = parsed.data
const models = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : []
return { success: true, data: models }
}
/**

View File

@@ -5,7 +5,7 @@
* 修改此处的版本号后,所有展示版本的地方都会自动更新
*/
export const APP_VERSION = '1.0.1'
export const APP_VERSION = '1.0.2'
export const APP_NAME = 'MaiBot Dashboard'
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`

View File

@@ -85,11 +85,11 @@ const modelConfigRoute = createRoute({
component: lazyRouteComponent(() => import('./routes/config/model'), 'ModelConfigPage'),
})
// 配置路由 - 麦麦适配器配置
// 配置路由 - 麦麦适配器配置(已停用,引导跳转到插件配置;旧实现保留在 ./routes/config/adapter
const adapterConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/adapter',
component: lazyRouteComponent(() => import('./routes/config/adapter'), 'AdapterConfigPage'),
component: lazyRouteComponent(() => import('./routes/config/adapter-disabled'), 'AdapterConfigPage'),
})
// 资源管理路由 - 表情包管理

View File

@@ -0,0 +1,60 @@
import { Link } from '@tanstack/react-router'
import { ArrowRight, Info } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
/**
* 麦麦适配器配置 —— 禁用页
*
* 原页面({@link import('./adapter').AdapterConfigPage})的能力已迁移至
* 「插件配置」中的对应适配器插件。这里保留路由占位并引导用户跳转,
* 避免误用旧的 TOML 直接编辑路径。
*/
export function AdapterConfigPage() {
return (
<ScrollArea className="h-full">
<div className="mx-auto w-full max-w-3xl space-y-4 p-4 sm:space-y-6 sm:p-6">
<div>
<h1 className="text-2xl font-bold sm:text-3xl"></h1>
<p className="text-muted-foreground mt-1 text-sm sm:mt-2 sm:text-base">
</p>
</div>
<Alert>
<Info className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
Napcat
</AlertDescription>
</Alert>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
TOML
</CardDescription>
</CardHeader>
<CardContent>
<Button asChild>
<Link to="/plugin-config">
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardContent>
</Card>
</div>
</ScrollArea>
)
}

View File

@@ -138,7 +138,11 @@ export function ProviderForm({
</DialogDescription>
</DialogHeader>
<form onSubmit={(e) => { e.preventDefault(); handleSaveEdit(); }} autoComplete="off">
<form
onSubmit={(e) => { e.preventDefault(); handleSaveEdit(); }}
autoComplete="off"
className="contents"
>
<DialogBody>
<div className="grid gap-4 py-4">
<div className="grid gap-2" data-tour="provider-template-select">

View File

@@ -16,6 +16,7 @@ interface InstalledTabProps {
checkPluginCompatibility: (plugin: PluginInfo) => boolean
needsUpdate: (plugin: PluginInfo) => boolean
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
getIncompatibleReason: (plugin: PluginInfo) => string | null
}
export function InstalledTab({
@@ -33,6 +34,7 @@ export function InstalledTab({
checkPluginCompatibility,
needsUpdate,
getStatusBadge,
getIncompatibleReason,
}: InstalledTabProps) {
// 过滤已安装插件
const filteredPlugins = plugins.filter(plugin => {
@@ -80,6 +82,7 @@ export function InstalledTab({
checkPluginCompatibility={checkPluginCompatibility}
needsUpdate={needsUpdate}
getStatusBadge={getStatusBadge}
getIncompatibleReason={getIncompatibleReason}
/>
))}
</div>

View File

@@ -16,6 +16,7 @@ interface MarketplaceTabProps {
checkPluginCompatibility: (plugin: PluginInfo) => boolean
needsUpdate: (plugin: PluginInfo) => boolean
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
getIncompatibleReason: (plugin: PluginInfo) => string | null
}
export function MarketplaceTab({
@@ -33,6 +34,7 @@ export function MarketplaceTab({
checkPluginCompatibility,
needsUpdate,
getStatusBadge,
getIncompatibleReason,
}: MarketplaceTabProps) {
// 过滤插件
const filteredPlugins = plugins.filter(plugin => {
@@ -76,6 +78,7 @@ export function MarketplaceTab({
checkPluginCompatibility={checkPluginCompatibility}
needsUpdate={needsUpdate}
getStatusBadge={getStatusBadge}
getIncompatibleReason={getIncompatibleReason}
/>
))}
</div>

View File

@@ -20,6 +20,7 @@ interface PluginCardProps {
checkPluginCompatibility: (plugin: PluginInfo) => boolean
needsUpdate: (plugin: PluginInfo) => boolean
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
getIncompatibleReason: (plugin: PluginInfo) => string | null
}
export function PluginCard({
@@ -34,6 +35,7 @@ export function PluginCard({
checkPluginCompatibility,
needsUpdate,
getStatusBadge,
getIncompatibleReason,
}: PluginCardProps) {
const navigate = useNavigate()
@@ -114,8 +116,14 @@ export function PluginCard({
needsUpdate(plugin) ? (
<Button
size="sm"
disabled={!gitStatus?.installed}
title={!gitStatus?.installed ? 'Git 未安装' : undefined}
disabled={!gitStatus?.installed || (maimaiVersion !== null && !checkPluginCompatibility(plugin))}
title={
!gitStatus?.installed
? 'Git 未安装'
: (maimaiVersion !== null && !checkPluginCompatibility(plugin))
? (getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容')
: undefined
}
onClick={() => onUpdate(plugin)}
>
<RefreshCw className="h-4 w-4 mr-1" />
@@ -145,7 +153,7 @@ export function PluginCard({
!gitStatus?.installed
? 'Git 未安装'
: (maimaiVersion !== null && !checkPluginCompatibility(plugin))
? `不兼容当前版本 (需要 ${plugin.manifest?.host_application?.min_version || '未知'}${plugin.manifest?.host_application?.max_version ? ` - ${plugin.manifest.host_application.max_version}` : '+'},当前 ${maimaiVersion?.version})`
? (getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容')
: undefined
}
onClick={() => onInstall(plugin)}

View File

@@ -268,8 +268,8 @@ function PluginsPageContent() {
// 获取插件状态徽章
const getStatusBadge = (plugin: PluginInfo) => {
// 优先显示兼容性状态
if (!plugin.installed && maimaiVersion && !checkPluginCompatibility(plugin)) {
// 优先显示兼容性状态(已安装但不兼容也需要提示,避免用户误以为可继续更新)
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
return (
<Badge variant="destructive" className="gap-1">
<AlertCircle className="h-3 w-3" />
@@ -317,9 +317,20 @@ function PluginsPageContent() {
}
// 检查插件兼容性
// 规则:
// 1. manifest_version === 1 的插件在麦麦 >= 1.0.0 时一律视为不兼容(旧 manifest 已不再被宿主接受);
// 2. 否则若声明了 host_application 范围,则按版本范围判定。
const checkPluginCompatibility = (plugin: PluginInfo): boolean => {
if (!maimaiVersion || !plugin.manifest?.host_application) return true
if (!maimaiVersion) return true
// manifest v1 在 1.0.0+ 麦麦上不再兼容
const manifestVersion = plugin.manifest?.manifest_version ?? 1
if (manifestVersion <= 1 && maimaiVersion.version_major >= 1) {
return false
}
if (!plugin.manifest?.host_application) return true
return isPluginCompatible(
plugin.manifest.host_application.min_version,
plugin.manifest.host_application.max_version,
@@ -327,11 +338,35 @@ function PluginsPageContent() {
)
}
// 不兼容原因(用于 UI 提示)
const getIncompatibleReason = (plugin: PluginInfo): string | null => {
if (!maimaiVersion) return null
const manifestVersion = plugin.manifest?.manifest_version ?? 1
if (manifestVersion <= 1 && maimaiVersion.version_major >= 1) {
return `该插件使用旧版 manifest (v${manifestVersion}),已不被麦麦 ${maimaiVersion.version} 支持`
}
if (plugin.manifest?.host_application && !isPluginCompatible(
plugin.manifest.host_application.min_version,
plugin.manifest.host_application.max_version,
maimaiVersion
)) {
const min = plugin.manifest.host_application.min_version || '未知'
const max = plugin.manifest.host_application.max_version
const range = max ? `${min} - ${max}` : `${min}+`
return `不兼容当前版本 (需要 ${range},当前 ${maimaiVersion.version})`
}
return null
}
// 检查是否需要更新(市场版本比已安装版本新)
const needsUpdate = (plugin: PluginInfo): boolean => {
if (!plugin.installed || !plugin.installed_version || !plugin.manifest?.version) {
return false
}
// 不兼容的插件不允许更新
if (!checkPluginCompatibility(plugin)) {
return false
}
const installedVer = plugin.installed_version.trim()
const marketVer = plugin.manifest.version.trim()
@@ -368,7 +403,7 @@ function PluginsPageContent() {
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
toast({
title: '无法安装',
description: '插件与当前麦麦版本不兼容',
description: getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容',
variant: 'destructive',
})
return
@@ -526,6 +561,16 @@ function PluginsPageContent() {
return
}
// 不兼容的插件不允许更新
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
toast({
title: '无法更新',
description: getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容',
variant: 'destructive',
})
return
}
try {
const updateResult = await updatePlugin(
plugin.id,
@@ -833,6 +878,7 @@ function PluginsPageContent() {
checkPluginCompatibility={checkPluginCompatibility}
needsUpdate={needsUpdate}
getStatusBadge={getStatusBadge}
getIncompatibleReason={getIncompatibleReason}
/>
) : activeTab === 'installed' ? (
<InstalledTab
@@ -850,6 +896,7 @@ function PluginsPageContent() {
checkPluginCompatibility={checkPluginCompatibility}
needsUpdate={needsUpdate}
getStatusBadge={getStatusBadge}
getIncompatibleReason={getIncompatibleReason}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">