Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
6
.github/workflows/docker-image-dev.yml
vendored
6
.github/workflows/docker-image-dev.yml
vendored
@@ -30,9 +30,6 @@ jobs:
|
|||||||
# - name: Clone maim_message
|
# - name: Clone maim_message
|
||||||
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
||||||
|
|
||||||
- name: Clone lpmm
|
|
||||||
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -84,9 +81,6 @@ jobs:
|
|||||||
# - name: Clone maim_message
|
# - name: Clone maim_message
|
||||||
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
||||||
|
|
||||||
- name: Clone lpmm
|
|
||||||
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/docker-image-main.yml
vendored
6
.github/workflows/docker-image-main.yml
vendored
@@ -34,9 +34,6 @@ jobs:
|
|||||||
# - name: Clone maim_message
|
# - name: Clone maim_message
|
||||||
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
||||||
|
|
||||||
- name: Clone lpmm
|
|
||||||
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -87,9 +84,6 @@ jobs:
|
|||||||
# - name: Clone maim_message
|
# - name: Clone maim_message
|
||||||
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
|
||||||
|
|
||||||
- name: Clone lpmm
|
|
||||||
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
35
Dockerfile
35
Dockerfile
@@ -1,44 +1,27 @@
|
|||||||
# 编译 LPMM
|
# Runtime image
|
||||||
FROM python:3.13-slim AS lpmm-builder
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
||||||
|
|
||||||
WORKDIR /MaiMBot-LPMM
|
|
||||||
|
|
||||||
# 同级目录下需要有 MaiMBot-LPMM
|
|
||||||
COPY MaiMBot-LPMM /MaiMBot-LPMM
|
|
||||||
|
|
||||||
# 安装编译器和编译依赖
|
|
||||||
RUN apt-get update && apt-get install -y build-essential
|
|
||||||
RUN uv pip install --system --upgrade pip
|
|
||||||
RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt
|
|
||||||
|
|
||||||
# 编译 LPMM
|
|
||||||
RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install
|
|
||||||
|
|
||||||
# 运行环境
|
|
||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
# 工作目录
|
# Working directory
|
||||||
WORKDIR /MaiMBot
|
WORKDIR /MaiMBot
|
||||||
|
|
||||||
ENV MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1
|
ENV MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1
|
||||||
|
|
||||||
# 复制依赖列表
|
# Copy dependency list
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y git
|
RUN apt-get update && apt-get install -y git
|
||||||
|
|
||||||
# 从编译阶段复制 LPMM 编译结果
|
# Install runtime dependencies
|
||||||
COPY --from=lpmm-builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
|
|
||||||
|
|
||||||
# 安装运行时依赖
|
|
||||||
RUN uv pip install --system --upgrade pip
|
RUN uv pip install --system --upgrade pip
|
||||||
RUN uv pip install --system -r requirements.txt
|
RUN uv pip install --system -r requirements.txt
|
||||||
|
|
||||||
# 复制项目代码
|
# Copy project source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN git clone --depth 1 --branch plugin https://github.com/Mai-with-u/MaiBot-Napcat-Adapter.git plugin-templates/MaiBot-Napcat-Adapter
|
||||||
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
ENTRYPOINT [ "python","bot.py" ]
|
ENTRYPOINT [ "./docker-entrypoint.sh" ]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "maibot-dashboard",
|
"name": "maibot-dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function Sidebar({
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
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',
|
inheritsPageBackground ? 'bg-transparent' : 'bg-card',
|
||||||
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
|
// 移动端始终显示完整宽度,桌面端根据 sidebarOpen 切换
|
||||||
'w-64 lg:w-auto',
|
'w-64 lg:w-auto',
|
||||||
@@ -46,9 +46,11 @@ export function Sidebar({
|
|||||||
|
|
||||||
<ScrollArea className={cn(
|
<ScrollArea className={cn(
|
||||||
'relative z-10',
|
'relative z-10',
|
||||||
"flex-1 overflow-x-hidden",
|
"min-h-0 flex-1 overflow-x-hidden",
|
||||||
!sidebarOpen && "lg:w-16"
|
!sidebarOpen && "lg:w-16"
|
||||||
)}>
|
)}
|
||||||
|
viewportClassName="[&>div]:!block"
|
||||||
|
>
|
||||||
<nav
|
<nav
|
||||||
aria-label={t('a11y.sidebarNav')}
|
aria-label={t('a11y.sidebarNav')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { useTour } from './use-tour'
|
|||||||
// Joyride 主题配置
|
// Joyride 主题配置
|
||||||
const joyrideStyles = {
|
const joyrideStyles = {
|
||||||
options: {
|
options: {
|
||||||
zIndex: 10000,
|
// 提到 portal 容器(99999)之上,确保 overlay/spotlight/tooltip 都在最上层;
|
||||||
|
// overlay 的 z-index 由 react-joyride 内部基于 options.zIndex 推算,必须大于 floater 才能让 tooltip 按钮可点击。
|
||||||
|
zIndex: 100000,
|
||||||
primaryColor: 'hsl(var(--color-primary))',
|
primaryColor: 'hsl(var(--color-primary))',
|
||||||
textColor: 'hsl(var(--color-foreground))',
|
textColor: 'hsl(var(--color-foreground))',
|
||||||
backgroundColor: 'hsl(var(--color-background))',
|
backgroundColor: 'hsl(var(--color-background))',
|
||||||
@@ -197,13 +199,6 @@ export function TourRenderer() {
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
scrollOffset={80}
|
scrollOffset={80}
|
||||||
scrollToFirstStep
|
scrollToFirstStep
|
||||||
floaterProps={{
|
|
||||||
styles: {
|
|
||||||
floater: {
|
|
||||||
zIndex: 99999,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
onPointerDownOutside={preventOutsideClose ? (e) => e.preventDefault() : undefined}
|
onPointerDownOutside={preventOutsideClose ? (e) => e.preventDefault() : undefined}
|
||||||
@@ -94,13 +94,17 @@ const DialogContent = React.forwardRef<
|
|||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(
|
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
|
<ScrollArea
|
||||||
ref={ref as never}
|
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)}
|
contentClassName={cn(allowHorizontalScroll && "min-w-full w-max", contentClassName)}
|
||||||
scrollbars={scrollbars ?? (allowHorizontalScroll ? "both" : "vertical")}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ const ScrollArea = React.forwardRef<
|
|||||||
className={cn("relative overflow-hidden", className)}
|
className={cn("relative overflow-hidden", className)}
|
||||||
{...props}
|
{...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>
|
<div className={contentClassName}>{children}</div>
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
{scrollbars !== "horizontal" && <ScrollBar />}
|
{scrollbars !== "horizontal" && <ScrollBar />}
|
||||||
|
|||||||
@@ -158,7 +158,14 @@ export async function fetchProviderModels(
|
|||||||
endpoint,
|
endpoint,
|
||||||
})
|
})
|
||||||
const response = await fetchWithAuth(`/api/webui/models/list?${params}`)
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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_NAME = 'MaiBot Dashboard'
|
||||||
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`
|
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`
|
||||||
|
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ const modelConfigRoute = createRoute({
|
|||||||
component: lazyRouteComponent(() => import('./routes/config/model'), 'ModelConfigPage'),
|
component: lazyRouteComponent(() => import('./routes/config/model'), 'ModelConfigPage'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 配置路由 - 麦麦适配器配置
|
// 配置路由 - 麦麦适配器配置(已停用,引导跳转到插件配置;旧实现保留在 ./routes/config/adapter)
|
||||||
const adapterConfigRoute = createRoute({
|
const adapterConfigRoute = createRoute({
|
||||||
getParentRoute: () => protectedRoute,
|
getParentRoute: () => protectedRoute,
|
||||||
path: '/config/adapter',
|
path: '/config/adapter',
|
||||||
component: lazyRouteComponent(() => import('./routes/config/adapter'), 'AdapterConfigPage'),
|
component: lazyRouteComponent(() => import('./routes/config/adapter-disabled'), 'AdapterConfigPage'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 资源管理路由 - 表情包管理
|
// 资源管理路由 - 表情包管理
|
||||||
|
|||||||
60
dashboard/src/routes/config/adapter-disabled.tsx
Normal file
60
dashboard/src/routes/config/adapter-disabled.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -138,7 +138,11 @@ export function ProviderForm({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={(e) => { e.preventDefault(); handleSaveEdit(); }} autoComplete="off">
|
<form
|
||||||
|
onSubmit={(e) => { e.preventDefault(); handleSaveEdit(); }}
|
||||||
|
autoComplete="off"
|
||||||
|
className="contents"
|
||||||
|
>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2" data-tour="provider-template-select">
|
<div className="grid gap-2" data-tour="provider-template-select">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface InstalledTabProps {
|
|||||||
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
||||||
needsUpdate: (plugin: PluginInfo) => boolean
|
needsUpdate: (plugin: PluginInfo) => boolean
|
||||||
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
||||||
|
getIncompatibleReason: (plugin: PluginInfo) => string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InstalledTab({
|
export function InstalledTab({
|
||||||
@@ -33,6 +34,7 @@ export function InstalledTab({
|
|||||||
checkPluginCompatibility,
|
checkPluginCompatibility,
|
||||||
needsUpdate,
|
needsUpdate,
|
||||||
getStatusBadge,
|
getStatusBadge,
|
||||||
|
getIncompatibleReason,
|
||||||
}: InstalledTabProps) {
|
}: InstalledTabProps) {
|
||||||
// 过滤已安装插件
|
// 过滤已安装插件
|
||||||
const filteredPlugins = plugins.filter(plugin => {
|
const filteredPlugins = plugins.filter(plugin => {
|
||||||
@@ -80,6 +82,7 @@ export function InstalledTab({
|
|||||||
checkPluginCompatibility={checkPluginCompatibility}
|
checkPluginCompatibility={checkPluginCompatibility}
|
||||||
needsUpdate={needsUpdate}
|
needsUpdate={needsUpdate}
|
||||||
getStatusBadge={getStatusBadge}
|
getStatusBadge={getStatusBadge}
|
||||||
|
getIncompatibleReason={getIncompatibleReason}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface MarketplaceTabProps {
|
|||||||
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
||||||
needsUpdate: (plugin: PluginInfo) => boolean
|
needsUpdate: (plugin: PluginInfo) => boolean
|
||||||
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
||||||
|
getIncompatibleReason: (plugin: PluginInfo) => string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarketplaceTab({
|
export function MarketplaceTab({
|
||||||
@@ -33,6 +34,7 @@ export function MarketplaceTab({
|
|||||||
checkPluginCompatibility,
|
checkPluginCompatibility,
|
||||||
needsUpdate,
|
needsUpdate,
|
||||||
getStatusBadge,
|
getStatusBadge,
|
||||||
|
getIncompatibleReason,
|
||||||
}: MarketplaceTabProps) {
|
}: MarketplaceTabProps) {
|
||||||
// 过滤插件
|
// 过滤插件
|
||||||
const filteredPlugins = plugins.filter(plugin => {
|
const filteredPlugins = plugins.filter(plugin => {
|
||||||
@@ -76,6 +78,7 @@ export function MarketplaceTab({
|
|||||||
checkPluginCompatibility={checkPluginCompatibility}
|
checkPluginCompatibility={checkPluginCompatibility}
|
||||||
needsUpdate={needsUpdate}
|
needsUpdate={needsUpdate}
|
||||||
getStatusBadge={getStatusBadge}
|
getStatusBadge={getStatusBadge}
|
||||||
|
getIncompatibleReason={getIncompatibleReason}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface PluginCardProps {
|
|||||||
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
checkPluginCompatibility: (plugin: PluginInfo) => boolean
|
||||||
needsUpdate: (plugin: PluginInfo) => boolean
|
needsUpdate: (plugin: PluginInfo) => boolean
|
||||||
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
getStatusBadge: (plugin: PluginInfo) => React.JSX.Element | null
|
||||||
|
getIncompatibleReason: (plugin: PluginInfo) => string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PluginCard({
|
export function PluginCard({
|
||||||
@@ -34,6 +35,7 @@ export function PluginCard({
|
|||||||
checkPluginCompatibility,
|
checkPluginCompatibility,
|
||||||
needsUpdate,
|
needsUpdate,
|
||||||
getStatusBadge,
|
getStatusBadge,
|
||||||
|
getIncompatibleReason,
|
||||||
}: PluginCardProps) {
|
}: PluginCardProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@@ -114,8 +116,14 @@ export function PluginCard({
|
|||||||
needsUpdate(plugin) ? (
|
needsUpdate(plugin) ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!gitStatus?.installed}
|
disabled={!gitStatus?.installed || (maimaiVersion !== null && !checkPluginCompatibility(plugin))}
|
||||||
title={!gitStatus?.installed ? 'Git 未安装' : undefined}
|
title={
|
||||||
|
!gitStatus?.installed
|
||||||
|
? 'Git 未安装'
|
||||||
|
: (maimaiVersion !== null && !checkPluginCompatibility(plugin))
|
||||||
|
? (getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => onUpdate(plugin)}
|
onClick={() => onUpdate(plugin)}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4 mr-1" />
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
@@ -145,7 +153,7 @@ export function PluginCard({
|
|||||||
!gitStatus?.installed
|
!gitStatus?.installed
|
||||||
? 'Git 未安装'
|
? 'Git 未安装'
|
||||||
: (maimaiVersion !== null && !checkPluginCompatibility(plugin))
|
: (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
|
: undefined
|
||||||
}
|
}
|
||||||
onClick={() => onInstall(plugin)}
|
onClick={() => onInstall(plugin)}
|
||||||
|
|||||||
@@ -268,8 +268,8 @@ function PluginsPageContent() {
|
|||||||
|
|
||||||
// 获取插件状态徽章
|
// 获取插件状态徽章
|
||||||
const getStatusBadge = (plugin: PluginInfo) => {
|
const getStatusBadge = (plugin: PluginInfo) => {
|
||||||
// 优先显示兼容性状态
|
// 优先显示兼容性状态(已安装但不兼容也需要提示,避免用户误以为可继续更新)
|
||||||
if (!plugin.installed && maimaiVersion && !checkPluginCompatibility(plugin)) {
|
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="destructive" className="gap-1">
|
<Badge variant="destructive" className="gap-1">
|
||||||
<AlertCircle className="h-3 w-3" />
|
<AlertCircle className="h-3 w-3" />
|
||||||
@@ -317,8 +317,19 @@ function PluginsPageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查插件兼容性
|
// 检查插件兼容性
|
||||||
|
// 规则:
|
||||||
|
// 1. manifest_version === 1 的插件在麦麦 >= 1.0.0 时一律视为不兼容(旧 manifest 已不再被宿主接受);
|
||||||
|
// 2. 否则若声明了 host_application 范围,则按版本范围判定。
|
||||||
const checkPluginCompatibility = (plugin: PluginInfo): boolean => {
|
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(
|
return isPluginCompatible(
|
||||||
plugin.manifest.host_application.min_version,
|
plugin.manifest.host_application.min_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 => {
|
const needsUpdate = (plugin: PluginInfo): boolean => {
|
||||||
if (!plugin.installed || !plugin.installed_version || !plugin.manifest?.version) {
|
if (!plugin.installed || !plugin.installed_version || !plugin.manifest?.version) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// 不兼容的插件不允许更新
|
||||||
|
if (!checkPluginCompatibility(plugin)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const installedVer = plugin.installed_version.trim()
|
const installedVer = plugin.installed_version.trim()
|
||||||
const marketVer = plugin.manifest.version.trim()
|
const marketVer = plugin.manifest.version.trim()
|
||||||
@@ -368,7 +403,7 @@ function PluginsPageContent() {
|
|||||||
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
|
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
|
||||||
toast({
|
toast({
|
||||||
title: '无法安装',
|
title: '无法安装',
|
||||||
description: '插件与当前麦麦版本不兼容',
|
description: getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -526,6 +561,16 @@ function PluginsPageContent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 不兼容的插件不允许更新
|
||||||
|
if (maimaiVersion && !checkPluginCompatibility(plugin)) {
|
||||||
|
toast({
|
||||||
|
title: '无法更新',
|
||||||
|
description: getIncompatibleReason(plugin) ?? '插件与当前麦麦版本不兼容',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateResult = await updatePlugin(
|
const updateResult = await updatePlugin(
|
||||||
plugin.id,
|
plugin.id,
|
||||||
@@ -833,6 +878,7 @@ function PluginsPageContent() {
|
|||||||
checkPluginCompatibility={checkPluginCompatibility}
|
checkPluginCompatibility={checkPluginCompatibility}
|
||||||
needsUpdate={needsUpdate}
|
needsUpdate={needsUpdate}
|
||||||
getStatusBadge={getStatusBadge}
|
getStatusBadge={getStatusBadge}
|
||||||
|
getIncompatibleReason={getIncompatibleReason}
|
||||||
/>
|
/>
|
||||||
) : activeTab === 'installed' ? (
|
) : activeTab === 'installed' ? (
|
||||||
<InstalledTab
|
<InstalledTab
|
||||||
@@ -850,6 +896,7 @@ function PluginsPageContent() {
|
|||||||
checkPluginCompatibility={checkPluginCompatibility}
|
checkPluginCompatibility={checkPluginCompatibility}
|
||||||
needsUpdate={needsUpdate}
|
needsUpdate={needsUpdate}
|
||||||
getStatusBadge={getStatusBadge}
|
getStatusBadge={getStatusBadge}
|
||||||
|
getIncompatibleReason={getIncompatibleReason}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
services:
|
services:
|
||||||
adapters:
|
|
||||||
container_name: maim-bot-adapters
|
|
||||||
#### prod ####
|
|
||||||
image: unclas/maimbot-adapter:latest
|
|
||||||
# image: infinitycat/maimbot-adapter:latest
|
|
||||||
#### dev ####
|
|
||||||
# image: unclas/maimbot-adapter:dev
|
|
||||||
# image: infinitycat/maimbot-adapter:dev
|
|
||||||
environment:
|
|
||||||
- TZ=Asia/Shanghai
|
|
||||||
# ports:
|
|
||||||
# - "8095:8095"
|
|
||||||
volumes:
|
|
||||||
- ./docker-config/adapters/config.toml:/adapters/config.toml # 持久化adapters配置文件
|
|
||||||
- ./data/adapters:/adapters/data # adapters 数据持久化
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- maim_bot
|
|
||||||
core:
|
core:
|
||||||
container_name: maim-bot-core
|
container_name: maim-bot-core
|
||||||
#### prod ####
|
#### prod ####
|
||||||
@@ -27,6 +9,8 @@ services:
|
|||||||
# image: infinitycat/maibot:dev
|
# image: infinitycat/maibot:dev
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
- EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d
|
||||||
|
- PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6
|
||||||
- MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1 # Docker 无法交互确认旧版升级迁移,默认跳过确认提示
|
- MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1 # Docker 无法交互确认旧版升级迁移,默认跳过确认提示
|
||||||
# - EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d # 同意EULA
|
# - EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d # 同意EULA
|
||||||
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
|
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
|
||||||
@@ -36,9 +20,9 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# 监听地址和端口已迁移到 ./docker-config/mmc/bot_config.toml 的 maim_message 与 webui 配置段
|
# 监听地址和端口已迁移到 ./docker-config/mmc/bot_config.toml 的 maim_message 与 webui 配置段
|
||||||
- ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
|
- ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
|
||||||
- ./docker-config/adapters:/MaiMBot/adapters-config # adapter配置文件夹映射
|
|
||||||
- ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
|
- ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
|
||||||
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
||||||
|
- ./data/MaiMBot/emoji:/data/emoji # 持久化表情包
|
||||||
- ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
|
- ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
|
||||||
- ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录
|
- ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录
|
||||||
# - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包,需要时启用
|
# - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包,需要时启用
|
||||||
|
|||||||
13
docker-entrypoint.sh
Normal file
13
docker-entrypoint.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ADAPTER_TEMPLATE="/MaiMBot/plugin-templates/MaiBot-Napcat-Adapter"
|
||||||
|
ADAPTER_TARGET="/MaiMBot/plugins/MaiBot-Napcat-Adapter"
|
||||||
|
|
||||||
|
mkdir -p /MaiMBot/plugins
|
||||||
|
|
||||||
|
if [ ! -e "$ADAPTER_TARGET" ] && [ -d "$ADAPTER_TEMPLATE" ]; then
|
||||||
|
cp -a "$ADAPTER_TEMPLATE" "$ADAPTER_TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python bot.py "$@"
|
||||||
@@ -19,8 +19,8 @@ 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.0.dev2026040439",
|
"maibot-dashboard==1.0.1.dev2026050251",
|
||||||
"maibot-plugin-sdk>=2.3.0",
|
"maibot-plugin-sdk>=2.4.0",
|
||||||
"matplotlib>=3.10.5",
|
"matplotlib>=3.10.5",
|
||||||
"mcp",
|
"mcp",
|
||||||
"msgpack>=1.1.2",
|
"msgpack>=1.1.2",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.core.tooling import ToolExecutionResult, ToolInvocation
|
from src.core.tooling import ToolExecutionResult, ToolInvocation
|
||||||
@@ -8,6 +9,7 @@ from src.llm_models.payload_content.tool_option import ToolCall
|
|||||||
from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService
|
from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService
|
||||||
from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
||||||
from src.maisaka.reasoning_engine import MaisakaReasoningEngine
|
from src.maisaka.reasoning_engine import MaisakaReasoningEngine
|
||||||
|
from src.maisaka.runtime import MaisakaHeartFlowChatting
|
||||||
|
|
||||||
|
|
||||||
def _build_chat_response(tool_calls: list[ToolCall]) -> ChatResponse:
|
def _build_chat_response(tool_calls: list[ToolCall]) -> ChatResponse:
|
||||||
@@ -173,6 +175,29 @@ def test_timing_gate_invalid_tool_hint_only_visible_to_timing_gate() -> None:
|
|||||||
assert planner_history == []
|
assert planner_history == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_forced_timing_trigger_bypasses_message_frequency_threshold() -> None:
|
||||||
|
runtime = SimpleNamespace(
|
||||||
|
_STATE_WAIT="wait",
|
||||||
|
_agent_state="stop",
|
||||||
|
_message_turn_scheduled=False,
|
||||||
|
_internal_turn_queue=asyncio.Queue(),
|
||||||
|
_has_pending_messages=lambda: True,
|
||||||
|
_get_pending_message_count=lambda: 1,
|
||||||
|
_has_forced_timing_trigger=lambda: True,
|
||||||
|
_cancel_deferred_message_turn_task=lambda: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fail_get_message_trigger_threshold() -> int:
|
||||||
|
raise AssertionError("@/提及必回不应被普通聊天频率阈值拦住")
|
||||||
|
|
||||||
|
runtime._get_message_trigger_threshold = _fail_get_message_trigger_threshold
|
||||||
|
|
||||||
|
MaisakaHeartFlowChatting._schedule_message_turn(runtime) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
assert runtime._message_turn_scheduled is True
|
||||||
|
assert runtime._internal_turn_queue.get_nowait() == "message"
|
||||||
|
|
||||||
|
|
||||||
def test_finish_tool_is_not_written_back_to_history() -> None:
|
def test_finish_tool_is_not_written_back_to_history() -> None:
|
||||||
finish_call = ToolCall(call_id="finish-call", func_name="finish", args={})
|
finish_call = ToolCall(call_id="finish-call", func_name="finish", args={})
|
||||||
reply_call = ToolCall(call_id="reply-call", func_name="reply", args={})
|
reply_call = ToolCall(call_id="reply-call", func_name="reply", args={})
|
||||||
@@ -213,3 +238,47 @@ def test_finish_tool_removes_empty_assistant_history_message() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert runtime._chat_history == []
|
assert runtime._chat_history == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_timing_gate_head_trim_keeps_short_history() -> None:
|
||||||
|
messages = [
|
||||||
|
AssistantMessage(content="第一条消息", timestamp=datetime.now()),
|
||||||
|
AssistantMessage(content="第二条消息", timestamp=datetime.now()),
|
||||||
|
]
|
||||||
|
|
||||||
|
trimmed_messages = MaisakaHeartFlowChatting._drop_head_context_messages(
|
||||||
|
messages,
|
||||||
|
drop_context_count=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trimmed_messages == messages
|
||||||
|
|
||||||
|
|
||||||
|
def test_timing_gate_head_trim_keeps_history_within_config_limit() -> None:
|
||||||
|
messages = [
|
||||||
|
AssistantMessage(content=f"消息 {index}", timestamp=datetime.now())
|
||||||
|
for index in range(10)
|
||||||
|
]
|
||||||
|
|
||||||
|
trimmed_messages = MaisakaHeartFlowChatting._drop_head_context_messages(
|
||||||
|
messages,
|
||||||
|
drop_context_count=7,
|
||||||
|
trim_threshold_context_count=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trimmed_messages == messages
|
||||||
|
|
||||||
|
|
||||||
|
def test_timing_gate_head_trim_applies_after_config_limit_exceeded() -> None:
|
||||||
|
messages = [
|
||||||
|
AssistantMessage(content=f"消息 {index}", timestamp=datetime.now())
|
||||||
|
for index in range(11)
|
||||||
|
]
|
||||||
|
|
||||||
|
trimmed_messages = MaisakaHeartFlowChatting._drop_head_context_messages(
|
||||||
|
messages,
|
||||||
|
drop_context_count=7,
|
||||||
|
trim_threshold_context_count=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trimmed_messages == messages[7:]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ httpx
|
|||||||
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-plugin-sdk>=1.2.3,<2.0.0
|
maibot-plugin-sdk>=2.4.0
|
||||||
mcp
|
mcp
|
||||||
msgpack>=1.1.2
|
msgpack>=1.1.2
|
||||||
numpy>=2.2.6
|
numpy>=2.2.6
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ def is_mentioned_bot_in_message(message: SessionMessage) -> tuple[bool, bool, fl
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 7) 概率设置
|
# 7) 概率设置
|
||||||
if is_at and getattr(global_config.chat, "at_bot_inevitable_reply", 1):
|
if is_at and getattr(global_config.chat, "inevitable_at_reply", 1):
|
||||||
reply_probability = 1.0
|
reply_probability = 1.0
|
||||||
logger.debug("被@,回复概率设置为100%")
|
logger.debug("被@,回复概率设置为100%")
|
||||||
elif is_mentioned and getattr(global_config.chat, "mentioned_bot_reply", 1):
|
elif is_mentioned and getattr(global_config.chat, "mentioned_bot_reply", 1):
|
||||||
|
|||||||
@@ -57,7 +57,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()
|
||||||
MMC_VERSION: str = "1.0.0"
|
MMC_VERSION: str = "1.0.0"
|
||||||
CONFIG_VERSION: str = "8.9.20"
|
CONFIG_VERSION: str = "8.9.20"
|
||||||
MODEL_CONFIG_VERSION: str = "1.14.3"
|
MODEL_CONFIG_VERSION: str = "1.14.5"
|
||||||
|
|
||||||
logger = get_logger("config")
|
logger = get_logger("config")
|
||||||
|
|
||||||
|
|||||||
@@ -11,26 +11,29 @@ DEFAULT_PROVIDER_TEMPLATES: list[dict[str, Any]] = [
|
|||||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
"api_key": "your-api-key",
|
"api_key": "your-api-key",
|
||||||
"auth_type": OpenAICompatibleAuthType.BEARER.value,
|
"auth_type": OpenAICompatibleAuthType.BEARER.value,
|
||||||
|
"max_retry": 3,
|
||||||
|
"timeout": 100,
|
||||||
|
"retry_interval": 8,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_TASK_CONFIG_TEMPLATES: dict[str, dict[str, Any]] = {
|
DEFAULT_TASK_CONFIG_TEMPLATES: dict[str, dict[str, Any]] = {
|
||||||
"utils": {
|
"utils": {
|
||||||
"model_list": ["qwen3.5-35b-a3b-nonthink"],
|
"model_list": ["deepseek-v4-flash"],
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"slow_threshold": 15.0,
|
"slow_threshold": 15.0,
|
||||||
"selection_strategy": "random",
|
"selection_strategy": "random",
|
||||||
},
|
},
|
||||||
"replyer": {
|
"replyer": {
|
||||||
"model_list": ["ali-glm-5"],
|
"model_list": ["deepseek-v4-pro-think", "deepseek-v4-pro-nonthink"],
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
"temperature": 1,
|
"temperature": 1,
|
||||||
"slow_threshold": 120.0,
|
"slow_threshold": 120.0,
|
||||||
"selection_strategy": "random",
|
"selection_strategy": "random",
|
||||||
},
|
},
|
||||||
"planner": {
|
"planner": {
|
||||||
"model_list": ["qwen3.5-35b-a3b", "qwen3.5-122b-a10b", "qwen3.5-flash"],
|
"model_list": ["deepseek-v4-flash"],
|
||||||
"max_tokens": 8000,
|
"max_tokens": 8000,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"slow_threshold": 12.0,
|
"slow_threshold": 12.0,
|
||||||
@@ -61,40 +64,30 @@ DEFAULT_TASK_CONFIG_TEMPLATES: dict[str, dict[str, Any]] = {
|
|||||||
|
|
||||||
DEFAULT_MODEL_TEMPLATES: list[dict[str, Any]] = [
|
DEFAULT_MODEL_TEMPLATES: list[dict[str, Any]] = [
|
||||||
{
|
{
|
||||||
"model_identifier": "glm-5",
|
"model_identifier": "deepseek-v4-pro",
|
||||||
"name": "ali-glm-5",
|
"name": "deepseek-v4-pro-think",
|
||||||
"api_provider": "BaiLian",
|
"api_provider": "BaiLian",
|
||||||
"price_in": 3.0,
|
"price_in": 12.0,
|
||||||
"price_out": 14.0,
|
"price_out": 24.0,
|
||||||
"temperature": 1.0,
|
|
||||||
"visual": False,
|
"visual": False,
|
||||||
"extra_params": {"enable_thinking": False},
|
"extra_params": {"enable_thinking": "True"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_identifier": "qwen3.5-122b-a10b",
|
"model_identifier": "deepseek-v4-pro",
|
||||||
"name": "qwen3.5-122b-a10b",
|
"name": "deepseek-v4-pro-nonthink",
|
||||||
"api_provider": "BaiLian",
|
"api_provider": "BaiLian",
|
||||||
"price_in": 0.8,
|
"price_in": 12.0,
|
||||||
"price_out": 6.4,
|
"price_out": 24.0,
|
||||||
"visual": True,
|
"visual": False,
|
||||||
"extra_params": {"enable_thinking": "false"},
|
"extra_params": {"enable_thinking": "false"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_identifier": "qwen3.5-35b-a3b",
|
"model_identifier": "deepseek-v4-flash",
|
||||||
"name": "qwen3.5-35b-a3b",
|
"name": "deepseek-v4-flash",
|
||||||
"api_provider": "BaiLian",
|
"api_provider": "BaiLian",
|
||||||
"price_in": 0.4,
|
"price_in": 1.0,
|
||||||
"price_out": 3.2,
|
"price_out": 2.0,
|
||||||
"visual": True,
|
"visual": False,
|
||||||
"extra_params": {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model_identifier": "qwen3.5-35b-a3b",
|
|
||||||
"name": "qwen3.5-35b-a3b-nonthink",
|
|
||||||
"api_provider": "BaiLian",
|
|
||||||
"price_in": 0.4,
|
|
||||||
"price_out": 3.2,
|
|
||||||
"visual": True,
|
|
||||||
"extra_params": {"enable_thinking": "false"},
|
"extra_params": {"enable_thinking": "false"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class APIProvider(ConfigBase):
|
|||||||
"""工具参数解析模式。可选值:`auto`、`strict`、`repair`、`double_decode`。"""
|
"""工具参数解析模式。可选值:`auto`、`strict`、`repair`、`double_decode`。"""
|
||||||
|
|
||||||
max_retry: int = Field(
|
max_retry: int = Field(
|
||||||
default=2,
|
default=3,
|
||||||
ge=0,
|
ge=0,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
@@ -182,7 +182,7 @@ class APIProvider(ConfigBase):
|
|||||||
"""最大重试次数 (单个模型API调用失败, 最多重试的次数)"""
|
"""最大重试次数 (单个模型API调用失败, 最多重试的次数)"""
|
||||||
|
|
||||||
timeout: int = Field(
|
timeout: int = Field(
|
||||||
default=10,
|
default=60,
|
||||||
ge=1,
|
ge=1,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
@@ -193,7 +193,7 @@ class APIProvider(ConfigBase):
|
|||||||
"""API调用的超时时长 (超过这个时长, 本次请求将被视为"请求超时", 单位: 秒)"""
|
"""API调用的超时时长 (超过这个时长, 本次请求将被视为"请求超时", 单位: 秒)"""
|
||||||
|
|
||||||
retry_interval: int = Field(
|
retry_interval: int = Field(
|
||||||
default=10,
|
default=5,
|
||||||
ge=1,
|
ge=1,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"x-widget": "input",
|
"x-widget": "input",
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ from collections.abc import Awaitable, Callable, Sequence
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
from src.chat.message_receive.chat_manager import chat_manager
|
from src.chat.message_receive.chat_manager import chat_manager
|
||||||
from src.cli.maisaka_cli_sender import CLI_PLATFORM_NAME, render_cli_message
|
from src.cli.maisaka_cli_sender import CLI_PLATFORM_NAME, render_cli_message
|
||||||
from src.common.data_models.image_data_model import MaiEmoji
|
from src.common.data_models.image_data_model import MaiEmoji
|
||||||
@@ -121,45 +119,13 @@ def _normalize_emotions(emoji: MaiEmoji) -> list[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def select_emoji_for_maisaka(
|
|
||||||
*,
|
|
||||||
requested_emotion: str = "",
|
|
||||||
reasoning: str = "",
|
|
||||||
context_texts: Sequence[str] | None = None,
|
|
||||||
sample_size: int = 30,
|
|
||||||
) -> tuple[MaiEmoji | None, str]:
|
|
||||||
"""为 Maisaka 选择一个合适的表情。"""
|
|
||||||
|
|
||||||
del reasoning, context_texts
|
|
||||||
|
|
||||||
available_emojis = list(emoji_manager.emojis)
|
|
||||||
if not available_emojis:
|
|
||||||
return None, ""
|
|
||||||
|
|
||||||
normalized_requested_emotion = requested_emotion.strip()
|
|
||||||
if normalized_requested_emotion:
|
|
||||||
matched_emojis = [
|
|
||||||
emoji
|
|
||||||
for emoji in available_emojis
|
|
||||||
if normalized_requested_emotion.lower() in (emotion.lower() for emotion in _normalize_emotions(emoji))
|
|
||||||
]
|
|
||||||
if matched_emojis:
|
|
||||||
return random.choice(matched_emojis), normalized_requested_emotion
|
|
||||||
|
|
||||||
sampled_emojis = random.sample(
|
|
||||||
available_emojis,
|
|
||||||
min(max(sample_size, 1), len(available_emojis)),
|
|
||||||
)
|
|
||||||
return random.choice(sampled_emojis), ""
|
|
||||||
|
|
||||||
|
|
||||||
async def send_emoji_for_maisaka(
|
async def send_emoji_for_maisaka(
|
||||||
*,
|
*,
|
||||||
stream_id: str,
|
stream_id: str,
|
||||||
|
emoji_selector: EmojiSelector,
|
||||||
requested_emotion: str = "",
|
requested_emotion: str = "",
|
||||||
reasoning: str = "",
|
reasoning: str = "",
|
||||||
context_texts: Sequence[str] | None = None,
|
context_texts: Sequence[str] | None = None,
|
||||||
emoji_selector: EmojiSelector | None = None,
|
|
||||||
) -> MaisakaEmojiSendResult:
|
) -> MaisakaEmojiSendResult:
|
||||||
"""为 Maisaka 选择并发送一个表情。"""
|
"""为 Maisaka 选择并发送一个表情。"""
|
||||||
|
|
||||||
@@ -194,20 +160,12 @@ async def send_emoji_for_maisaka(
|
|||||||
normalized_context_texts = _normalize_context_texts(before_select_kwargs.get("context_texts"))
|
normalized_context_texts = _normalize_context_texts(before_select_kwargs.get("context_texts"))
|
||||||
sample_size = _coerce_positive_int(before_select_kwargs.get("sample_size"), sample_size)
|
sample_size = _coerce_positive_int(before_select_kwargs.get("sample_size"), sample_size)
|
||||||
|
|
||||||
if emoji_selector is None:
|
selected_emoji, matched_emotion = await emoji_selector(
|
||||||
selected_emoji, matched_emotion = await select_emoji_for_maisaka(
|
normalized_requested_emotion,
|
||||||
requested_emotion=normalized_requested_emotion,
|
normalized_reasoning,
|
||||||
reasoning=normalized_reasoning,
|
normalized_context_texts,
|
||||||
context_texts=normalized_context_texts,
|
sample_size,
|
||||||
sample_size=sample_size,
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
selected_emoji, matched_emotion = await emoji_selector(
|
|
||||||
normalized_requested_emotion,
|
|
||||||
normalized_reasoning,
|
|
||||||
normalized_context_texts,
|
|
||||||
sample_size,
|
|
||||||
)
|
|
||||||
after_select_result = await _get_runtime_manager().invoke_hook(
|
after_select_result = await _get_runtime_manager().invoke_hook(
|
||||||
"emoji.maisaka.after_select",
|
"emoji.maisaka.after_select",
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from json import dumps
|
||||||
from random import sample
|
from random import sample
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
@@ -17,9 +18,8 @@ from src.emoji_system.maisaka_tool import send_emoji_for_maisaka
|
|||||||
from src.common.data_models.image_data_model import MaiEmoji
|
from src.common.data_models.image_data_model import MaiEmoji
|
||||||
from src.common.data_models.message_component_data_model import ImageComponent, MessageSequence, TextComponent
|
from src.common.data_models.message_component_data_model import ImageComponent, MessageSequence, TextComponent
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.config.config import global_config
|
from src.config.config import config_manager, global_config
|
||||||
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
|
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
|
||||||
from src.llm_models.payload_content.resp_format import RespFormat, RespFormatType
|
|
||||||
from src.llm_models.payload_content.message import MessageBuilder, RoleType
|
from src.llm_models.payload_content.message import MessageBuilder, RoleType
|
||||||
from src.maisaka.context_messages import (
|
from src.maisaka.context_messages import (
|
||||||
LLMContextMessage,
|
LLMContextMessage,
|
||||||
@@ -221,6 +221,7 @@ def _build_send_emoji_monitor_detail(
|
|||||||
detail: Dict[str, Any] = {}
|
detail: Dict[str, Any] = {}
|
||||||
if isinstance(request_messages, list) and request_messages:
|
if isinstance(request_messages, list) and request_messages:
|
||||||
detail["request_messages"] = request_messages
|
detail["request_messages"] = request_messages
|
||||||
|
detail["prompt_text"] = dumps(request_messages, ensure_ascii=False, indent=2)
|
||||||
if reasoning_text.strip():
|
if reasoning_text.strip():
|
||||||
detail["reasoning_text"] = reasoning_text.strip()
|
detail["reasoning_text"] = reasoning_text.strip()
|
||||||
if output_text.strip():
|
if output_text.strip():
|
||||||
@@ -279,6 +280,24 @@ def _build_send_emoji_monitor_metadata(
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_emoji_selector_model_task_name() -> str:
|
||||||
|
"""根据 planner 模型视觉能力选择表情选择子代理的模型任务。"""
|
||||||
|
|
||||||
|
model_config = config_manager.get_model_config()
|
||||||
|
planner_models = [
|
||||||
|
model_name
|
||||||
|
for model_name in model_config.model_task_config.planner.model_list
|
||||||
|
if str(model_name).strip()
|
||||||
|
]
|
||||||
|
models_by_name = {model.name: model for model in model_config.models}
|
||||||
|
if planner_models and all(
|
||||||
|
model_name in models_by_name and models_by_name[model_name].visual
|
||||||
|
for model_name in planner_models
|
||||||
|
):
|
||||||
|
return "planner"
|
||||||
|
return "vlm"
|
||||||
|
|
||||||
|
|
||||||
async def _select_emoji_with_sub_agent(
|
async def _select_emoji_with_sub_agent(
|
||||||
tool_ctx: BuiltinToolRuntimeContext,
|
tool_ctx: BuiltinToolRuntimeContext,
|
||||||
reasoning: str,
|
reasoning: str,
|
||||||
@@ -326,7 +345,8 @@ async def _select_emoji_with_sub_agent(
|
|||||||
prompt_llm_message = prompt_message.to_llm_message()
|
prompt_llm_message = prompt_message.to_llm_message()
|
||||||
if prompt_llm_message is not None:
|
if prompt_llm_message is not None:
|
||||||
request_messages.append(prompt_llm_message)
|
request_messages.append(prompt_llm_message)
|
||||||
candidate_llm_message = candidate_message.to_llm_message()
|
candidate_to_llm_message = getattr(candidate_message, "to_llm_message", None)
|
||||||
|
candidate_llm_message = candidate_to_llm_message() if callable(candidate_to_llm_message) else None
|
||||||
if candidate_llm_message is not None:
|
if candidate_llm_message is not None:
|
||||||
request_messages.append(candidate_llm_message)
|
request_messages.append(candidate_llm_message)
|
||||||
serialized_request_messages = serialize_prompt_messages(request_messages)
|
serialized_request_messages = serialize_prompt_messages(request_messages)
|
||||||
@@ -337,10 +357,7 @@ async def _select_emoji_with_sub_agent(
|
|||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
extra_messages=[prompt_message, candidate_message],
|
extra_messages=[prompt_message, candidate_message],
|
||||||
max_tokens=_EMOJI_SUB_AGENT_MAX_TOKENS,
|
max_tokens=_EMOJI_SUB_AGENT_MAX_TOKENS,
|
||||||
response_format=RespFormat(
|
model_task_name=_resolve_emoji_selector_model_task_name(),
|
||||||
format_type=RespFormatType.JSON_SCHEMA,
|
|
||||||
schema=EmojiSelectionResult,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
selection_duration_ms = round((datetime.now() - selection_started_at).total_seconds() * 1000, 2)
|
selection_duration_ms = round((datetime.now() - selection_started_at).total_seconds() * 1000, 2)
|
||||||
|
|
||||||
@@ -409,12 +426,16 @@ async def handle_tool(
|
|||||||
"reason": "",
|
"reason": "",
|
||||||
}
|
}
|
||||||
selection_metadata: Dict[str, Any] = {"reason": "", "monitor_detail": {}}
|
selection_metadata: Dict[str, Any] = {"reason": "", "monitor_detail": {}}
|
||||||
|
requested_emotion = ""
|
||||||
|
if isinstance(invocation.arguments, dict):
|
||||||
|
requested_emotion = str(invocation.arguments.get("emotion") or "").strip()
|
||||||
|
|
||||||
logger.info(f"{tool_ctx.runtime.log_prefix} 触发表情包发送工具")
|
logger.info(f"{tool_ctx.runtime.log_prefix} 触发表情包发送工具")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_result = await send_emoji_for_maisaka(
|
send_result = await send_emoji_for_maisaka(
|
||||||
stream_id=tool_ctx.runtime.session_id,
|
stream_id=tool_ctx.runtime.session_id,
|
||||||
|
requested_emotion=requested_emotion,
|
||||||
reasoning=tool_ctx.engine.last_reasoning_content,
|
reasoning=tool_ctx.engine.last_reasoning_content,
|
||||||
context_texts=context_texts,
|
context_texts=context_texts,
|
||||||
emoji_selector=lambda _requested_emotion, reasoning, context_texts, sample_size: _select_emoji_with_sub_agent(
|
emoji_selector=lambda _requested_emotion, reasoning, context_texts, sample_size: _select_emoji_with_sub_agent(
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ class MaisakaChatLoopService:
|
|||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
is_group_chat: Optional[bool] = None,
|
is_group_chat: Optional[bool] = None,
|
||||||
max_tokens: int = 2048,
|
max_tokens: int = 2048,
|
||||||
|
model_task_name: str = "planner",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""初始化 Maisaka 对话循环服务。
|
"""初始化 Maisaka 对话循环服务。
|
||||||
|
|
||||||
@@ -205,6 +206,7 @@ class MaisakaChatLoopService:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._max_tokens = max_tokens
|
self._max_tokens = max_tokens
|
||||||
|
self._model_task_name = model_task_name.strip() or "planner"
|
||||||
self._is_group_chat = is_group_chat
|
self._is_group_chat = is_group_chat
|
||||||
self._session_id = session_id or ""
|
self._session_id = session_id or ""
|
||||||
self._extra_tools: List[ToolOption] = []
|
self._extra_tools: List[ToolOption] = []
|
||||||
@@ -236,17 +238,18 @@ class MaisakaChatLoopService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_llm_chat_client(self, request_kind: str) -> LLMServiceClient:
|
def _get_llm_chat_client(self, request_kind: str) -> LLMServiceClient:
|
||||||
"""获取当前请求类型对应的 planner LLM 客户端。"""
|
"""获取当前请求类型对应的 LLM 客户端。"""
|
||||||
|
|
||||||
request_type = self._resolve_llm_request_type(request_kind)
|
request_type = self._resolve_llm_request_type(request_kind)
|
||||||
llm_client = self._llm_chat_clients.get(request_type)
|
client_key = f"{self._model_task_name}:{request_type}"
|
||||||
|
llm_client = self._llm_chat_clients.get(client_key)
|
||||||
if llm_client is None:
|
if llm_client is None:
|
||||||
llm_client = LLMServiceClient(
|
llm_client = LLMServiceClient(
|
||||||
task_name="planner",
|
task_name=self._model_task_name,
|
||||||
request_type=request_type,
|
request_type=request_type,
|
||||||
session_id=self._session_id,
|
session_id=self._session_id,
|
||||||
)
|
)
|
||||||
self._llm_chat_clients[request_type] = llm_client
|
self._llm_chat_clients[client_key] = llm_client
|
||||||
return llm_client
|
return llm_client
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -473,13 +473,18 @@ class MaisakaHeartFlowChatting:
|
|||||||
def _update_message_trigger_state(self, message: SessionMessage) -> None:
|
def _update_message_trigger_state(self, message: SessionMessage) -> None:
|
||||||
"""补齐消息中的 @/提及 标记,并在命中时启用强制 continue。"""
|
"""补齐消息中的 @/提及 标记,并在命中时启用强制 continue。"""
|
||||||
|
|
||||||
detected_mentioned, detected_at, _ = is_mentioned_bot_in_message(message)
|
detected_mentioned, detected_at, reply_probability_boost = is_mentioned_bot_in_message(message)
|
||||||
if detected_at:
|
if detected_at:
|
||||||
message.is_at = True
|
message.is_at = True
|
||||||
if detected_mentioned:
|
if detected_mentioned:
|
||||||
message.is_mentioned = True
|
message.is_mentioned = True
|
||||||
|
|
||||||
if not message.is_at and not message.is_mentioned:
|
should_force_reply = (
|
||||||
|
reply_probability_boost >= 1.0
|
||||||
|
or (message.is_at and global_config.chat.inevitable_at_reply)
|
||||||
|
or (message.is_mentioned and global_config.chat.mentioned_bot_reply)
|
||||||
|
)
|
||||||
|
if not should_force_reply or (not message.is_at and not message.is_mentioned):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._arm_force_next_timing_continue(
|
self._arm_force_next_timing_continue(
|
||||||
@@ -537,6 +542,11 @@ class MaisakaHeartFlowChatting:
|
|||||||
self._force_next_timing_reason = ""
|
self._force_next_timing_reason = ""
|
||||||
return reason
|
return reason
|
||||||
|
|
||||||
|
def _has_forced_timing_trigger(self) -> bool:
|
||||||
|
"""判断是否已有 @/提及必回触发,需绕过普通频率阈值。"""
|
||||||
|
|
||||||
|
return self._force_next_timing_continue
|
||||||
|
|
||||||
def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None:
|
def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None:
|
||||||
"""绑定当前可打断请求使用的中断标记。"""
|
"""绑定当前可打断请求使用的中断标记。"""
|
||||||
self._planner_interrupt_flag = interrupt_flag
|
self._planner_interrupt_flag = interrupt_flag
|
||||||
@@ -590,6 +600,7 @@ class MaisakaHeartFlowChatting:
|
|||||||
extra_messages: Optional[Sequence[LLMContextMessage]] = None,
|
extra_messages: Optional[Sequence[LLMContextMessage]] = None,
|
||||||
interrupt_flag: asyncio.Event | None = None,
|
interrupt_flag: asyncio.Event | None = None,
|
||||||
max_tokens: int = 512,
|
max_tokens: int = 512,
|
||||||
|
model_task_name: str = "planner",
|
||||||
response_format: RespFormat | None = None,
|
response_format: RespFormat | None = None,
|
||||||
tool_definitions: Optional[Sequence[ToolDefinitionInput]] = None,
|
tool_definitions: Optional[Sequence[ToolDefinitionInput]] = None,
|
||||||
) -> ChatResponse:
|
) -> ChatResponse:
|
||||||
@@ -603,6 +614,7 @@ class MaisakaHeartFlowChatting:
|
|||||||
sub_agent_history = self._drop_head_context_messages(
|
sub_agent_history = self._drop_head_context_messages(
|
||||||
selected_history,
|
selected_history,
|
||||||
drop_head_context_count,
|
drop_head_context_count,
|
||||||
|
trim_threshold_context_count=context_message_limit,
|
||||||
)
|
)
|
||||||
if extra_messages:
|
if extra_messages:
|
||||||
sub_agent_history.extend(list(extra_messages))
|
sub_agent_history.extend(list(extra_messages))
|
||||||
@@ -612,6 +624,7 @@ class MaisakaHeartFlowChatting:
|
|||||||
session_id=self.session_id,
|
session_id=self.session_id,
|
||||||
is_group_chat=self.chat_stream.is_group_session,
|
is_group_chat=self.chat_stream.is_group_session,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
|
model_task_name=model_task_name,
|
||||||
)
|
)
|
||||||
sub_agent.set_interrupt_flag(interrupt_flag)
|
sub_agent.set_interrupt_flag(interrupt_flag)
|
||||||
return await sub_agent.chat_loop_step(
|
return await sub_agent.chat_loop_step(
|
||||||
@@ -625,12 +638,21 @@ class MaisakaHeartFlowChatting:
|
|||||||
def _drop_head_context_messages(
|
def _drop_head_context_messages(
|
||||||
chat_history: Sequence[LLMContextMessage],
|
chat_history: Sequence[LLMContextMessage],
|
||||||
drop_context_count: int,
|
drop_context_count: int,
|
||||||
|
*,
|
||||||
|
trim_threshold_context_count: int | None = None,
|
||||||
) -> list[LLMContextMessage]:
|
) -> list[LLMContextMessage]:
|
||||||
"""从已选上下文头部丢弃指定数量的普通上下文消息。"""
|
"""从已选上下文头部丢弃指定数量的普通上下文消息。"""
|
||||||
|
|
||||||
if drop_context_count <= 0:
|
if drop_context_count <= 0:
|
||||||
return list(chat_history)
|
return list(chat_history)
|
||||||
|
|
||||||
|
context_message_count = sum(1 for message in chat_history if message.count_in_context)
|
||||||
|
if trim_threshold_context_count is not None and context_message_count <= trim_threshold_context_count:
|
||||||
|
return list(chat_history)
|
||||||
|
|
||||||
|
if context_message_count <= drop_context_count:
|
||||||
|
return list(chat_history)
|
||||||
|
|
||||||
first_kept_index = 0
|
first_kept_index = 0
|
||||||
dropped_context_count = 0
|
dropped_context_count = 0
|
||||||
while (
|
while (
|
||||||
@@ -867,6 +889,12 @@ class MaisakaHeartFlowChatting:
|
|||||||
if pending_count <= 0:
|
if pending_count <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._has_forced_timing_trigger():
|
||||||
|
self._cancel_deferred_message_turn_task()
|
||||||
|
self._message_turn_scheduled = True
|
||||||
|
self._internal_turn_queue.put_nowait("message")
|
||||||
|
return
|
||||||
|
|
||||||
trigger_threshold = self._get_message_trigger_threshold()
|
trigger_threshold = self._get_message_trigger_threshold()
|
||||||
if pending_count >= trigger_threshold or self._should_trigger_message_turn_by_idle_compensation(
|
if pending_count >= trigger_threshold or self._should_trigger_message_turn_by_idle_compensation(
|
||||||
pending_count=pending_count,
|
pending_count=pending_count,
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ async def _fetch_models_from_provider(
|
|||||||
client_config = build_openai_compatible_client_config(provider)
|
client_config = build_openai_compatible_client_config(provider)
|
||||||
headers.update(client_config.default_headers)
|
headers.update(client_config.default_headers)
|
||||||
params.update(client_config.default_query)
|
params.update(client_config.default_query)
|
||||||
|
# build_openai_compatible_client_config 在“默认 Bearer”场景下,
|
||||||
|
# 会把 api_key 留在 client_config.api_key 中交给 OpenAI SDK 自行注入 Authorization 头,
|
||||||
|
# 而不会写入 default_headers。这里我们用 httpx 直接发请求,需要手动补上鉴权头/参数。
|
||||||
|
if client_config.api_key and "Authorization" not in headers:
|
||||||
|
headers["Authorization"] = f"Bearer {client_config.api_key}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
|||||||
Reference in New Issue
Block a user