@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "maibot-dashboard",
|
"name": "maibot-dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface PluginApiResponse {
|
|||||||
id: string
|
id: string
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
@@ -56,6 +57,7 @@ function normalizePluginManifest(manifest: PluginApiResponse['manifest']): Plugi
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
manifest_version: manifest.manifest_version || 1,
|
manifest_version: manifest.manifest_version || 1,
|
||||||
|
id: manifest.id,
|
||||||
name: manifest.name,
|
name: manifest.name,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
description: manifest.description || '',
|
description: manifest.description || '',
|
||||||
@@ -104,10 +106,15 @@ export async function fetchPluginList(): Promise<ApiResponse<PluginInfo[]>> {
|
|||||||
|
|
||||||
const pluginList = data
|
const pluginList = data
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
if (!item?.id || !item?.manifest) {
|
if (!item?.manifest) {
|
||||||
console.warn('跳过无效插件数据:', item)
|
console.warn('跳过无效插件数据:', item)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
const pluginId = item.manifest.id || item.id
|
||||||
|
if (!pluginId) {
|
||||||
|
console.warn('跳过缺少 ID 的插件:', item)
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!item.manifest.name || !item.manifest.version) {
|
if (!item.manifest.name || !item.manifest.version) {
|
||||||
console.warn('跳过缺少必需字段的插件:', item.id)
|
console.warn('跳过缺少必需字段的插件:', item.id)
|
||||||
return false
|
return false
|
||||||
@@ -115,7 +122,7 @@ export async function fetchPluginList(): Promise<ApiResponse<PluginInfo[]>> {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
id: item.id,
|
id: item.manifest.id || item.id,
|
||||||
manifest: normalizePluginManifest(item.manifest),
|
manifest: normalizePluginManifest(item.manifest),
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
rating: 0,
|
rating: 0,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface InstalledPlugin {
|
|||||||
id: string
|
id: string
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 修改此处的版本号后,所有展示版本的地方都会自动更新
|
* 修改此处的版本号后,所有展示版本的地方都会自动更新
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const APP_VERSION = '1.0.9'
|
export const APP_VERSION = '1.0.10'
|
||||||
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}`
|
||||||
|
|
||||||
|
|||||||
@@ -104,21 +104,23 @@ export function PluginDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pluginList = JSON.parse(result.data)
|
const pluginList = JSON.parse(result.data)
|
||||||
const foundPlugin = pluginList.find((p: any) => p.id === search.pluginId)
|
const foundPlugin = pluginList.find((p: any) => (p.manifest?.id || p.id) === search.pluginId)
|
||||||
|
|
||||||
if (!foundPlugin) {
|
if (!foundPlugin) {
|
||||||
throw new Error('未找到该插件')
|
throw new Error('未找到该插件')
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawManifest = foundPlugin.manifest || {}
|
const rawManifest = foundPlugin.manifest || {}
|
||||||
|
const pluginId = rawManifest.id || foundPlugin.id
|
||||||
const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository
|
const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository
|
||||||
const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage
|
const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage
|
||||||
|
|
||||||
// 转换为 PluginInfo 格式
|
// 转换为 PluginInfo 格式
|
||||||
const pluginInfo: PluginInfo = {
|
const pluginInfo: PluginInfo = {
|
||||||
id: foundPlugin.id,
|
id: pluginId,
|
||||||
manifest: {
|
manifest: {
|
||||||
...rawManifest,
|
...rawManifest,
|
||||||
|
id: pluginId,
|
||||||
homepage_url: homepageUrl,
|
homepage_url: homepageUrl,
|
||||||
repository_url: repositoryUrl,
|
repository_url: repositoryUrl,
|
||||||
default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN',
|
default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN',
|
||||||
@@ -170,8 +172,8 @@ export function PluginDetailPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsInstalled(checkPluginInstalled(search.pluginId, installedPlugins.data))
|
setIsInstalled(checkPluginInstalled(pluginId, installedPlugins.data))
|
||||||
setInstalledVersion(getInstalledPluginVersion(search.pluginId, installedPlugins.data))
|
setInstalledVersion(getInstalledPluginVersion(pluginId, installedPlugins.data))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '加载失败')
|
setError(err instanceof Error ? err.message : '加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -196,7 +198,7 @@ export function PluginDetailPage() {
|
|||||||
// 如果插件已安装,优先尝试从本地读取 README
|
// 如果插件已安装,优先尝试从本地读取 README
|
||||||
if (isInstalled && search.pluginId) {
|
if (isInstalled && search.pluginId) {
|
||||||
try {
|
try {
|
||||||
const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${search.pluginId}`)
|
const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${plugin.id}`)
|
||||||
|
|
||||||
if (localResponse.ok) {
|
if (localResponse.ok) {
|
||||||
const localResult = await localResponse.json()
|
const localResult = await localResponse.json()
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function InstalledTab({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 xl:grid-cols-3 2xl:grid-cols-4 gap-5">
|
||||||
{filteredPlugins.map((plugin) => (
|
{filteredPlugins.map((plugin) => (
|
||||||
<PluginCard
|
<PluginCard
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function MarketplaceTab({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 xl:grid-cols-3 2xl:grid-cols-4 gap-5">
|
||||||
{filteredPlugins.map((plugin) => (
|
{filteredPlugins.map((plugin) => (
|
||||||
<PluginCard
|
<PluginCard
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ export function PluginCard({
|
|||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
className="flex flex-col hover:shadow-lg transition-shadow h-full"
|
className="flex flex-col hover:shadow-lg transition-shadow h-full"
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader className="p-5 pb-3">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<CardTitle className="text-xl">{plugin.manifest?.name || plugin.id}</CardTitle>
|
<CardTitle className="text-lg leading-snug">{plugin.manifest?.name || plugin.id}</CardTitle>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||||
{plugin.manifest?.categories && plugin.manifest.categories[0] && (
|
{plugin.manifest?.categories && plugin.manifest.categories[0] && (
|
||||||
<Badge variant="secondary" className="text-xs whitespace-nowrap">
|
<Badge variant="secondary" className="text-xs whitespace-nowrap">
|
||||||
{CATEGORY_NAMES[plugin.manifest.categories[0]] || plugin.manifest.categories[0]}
|
{CATEGORY_NAMES[plugin.manifest.categories[0]] || plugin.manifest.categories[0]}
|
||||||
@@ -56,18 +56,18 @@ export function PluginCard({
|
|||||||
{getStatusBadge(plugin)}
|
{getStatusBadge(plugin)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="line-clamp-2">{plugin.manifest?.description || '无描述'}</CardDescription>
|
<CardDescription className="line-clamp-2 text-sm leading-snug">{plugin.manifest?.description || '无描述'}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1">
|
<CardContent className="px-5 pb-3">
|
||||||
<div className="space-y-3">
|
<div className="space-y-2.5">
|
||||||
{/* 统计信息 */}
|
{/* 统计信息 */}
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
<span>{(pluginStats[plugin.id]?.downloads ?? plugin.downloads ?? 0).toLocaleString()}</span>
|
<span>{(pluginStats[plugin.id]?.downloads ?? plugin.downloads ?? 0).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
|
<Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400" />
|
||||||
<span>{(pluginStats[plugin.id]?.rating ?? plugin.rating ?? 0).toFixed(1)}</span>
|
<span>{(pluginStats[plugin.id]?.rating ?? plugin.rating ?? 0).toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,7 @@ export function PluginCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 版本和作者 */}
|
{/* 版本和作者 */}
|
||||||
<div className="text-xs text-muted-foreground pt-2 border-t space-y-1">
|
<div className="text-xs text-muted-foreground pt-2.5 border-t space-y-1">
|
||||||
<div>v{plugin.manifest?.version || 'unknown'} · {plugin.manifest?.author?.name || 'Unknown'}</div>
|
<div>v{plugin.manifest?.version || 'unknown'} · {plugin.manifest?.author?.name || 'Unknown'}</div>
|
||||||
{/* 支持版本 */}
|
{/* 支持版本 */}
|
||||||
{plugin.manifest?.host_application && (
|
{plugin.manifest?.host_application && (
|
||||||
@@ -103,7 +103,7 @@ export function PluginCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="pt-4">
|
<CardFooter className="px-5 pt-2 pb-5">
|
||||||
<div className="flex items-center justify-end gap-2 w-full">
|
<div className="flex items-center justify-end gap-2 w-full">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -169,7 +169,7 @@ export function PluginCard({
|
|||||||
(loadProgress.stage === 'loading' || loadProgress.stage === 'success' || loadProgress.stage === 'error') &&
|
(loadProgress.stage === 'loading' || loadProgress.stage === 'success' || loadProgress.stage === 'error') &&
|
||||||
loadProgress.operation !== 'fetch' &&
|
loadProgress.operation !== 'fetch' &&
|
||||||
loadProgress.plugin_id === plugin.id && (
|
loadProgress.plugin_id === plugin.id && (
|
||||||
<div className="px-6 pb-4 -mt-2">
|
<div className="px-5 pb-5 -mt-1">
|
||||||
<div className={`space-y-2 p-3 rounded-lg border ${
|
<div className={`space-y-2 p-3 rounded-lg border ${
|
||||||
loadProgress.stage === 'success'
|
loadProgress.stage === 'success'
|
||||||
? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-900'
|
? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-900'
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export function PluginsPage() {
|
|||||||
function PluginsPageContent() {
|
function PluginsPageContent() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { triggerRestart, isRestarting } = useRestart()
|
const { triggerRestart, isRestarting } = useRestart()
|
||||||
|
const [restartNoticeVisible, setRestartNoticeVisible] = useState(
|
||||||
|
() => localStorage.getItem('plugins-restart-notice-dismissed') !== 'true'
|
||||||
|
)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [categoryFilter, setCategoryFilter] = useState('all')
|
const [categoryFilter, setCategoryFilter] = useState('all')
|
||||||
const [activeTab, setActiveTab] = useState('all') // all | installed | updates
|
const [activeTab, setActiveTab] = useState('all') // all | installed | updates
|
||||||
@@ -67,6 +70,11 @@ function PluginsPageContent() {
|
|||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
|
const dismissRestartNotice = () => {
|
||||||
|
localStorage.setItem('plugins-restart-notice-dismissed', 'true')
|
||||||
|
setRestartNoticeVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载插件统计数据
|
// 加载插件统计数据
|
||||||
const loadPluginStats = async (pluginList: PluginInfo[]) => {
|
const loadPluginStats = async (pluginList: PluginInfo[]) => {
|
||||||
const statsPromises = pluginList.map(async (plugin) => {
|
const statsPromises = pluginList.map(async (plugin) => {
|
||||||
@@ -220,6 +228,7 @@ function PluginsPageContent() {
|
|||||||
id: installedPlugin.id,
|
id: installedPlugin.id,
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: installedPlugin.manifest.manifest_version || 1,
|
manifest_version: installedPlugin.manifest.manifest_version || 1,
|
||||||
|
id: installedPlugin.manifest.id || installedPlugin.id,
|
||||||
name: installedPlugin.manifest.name,
|
name: installedPlugin.manifest.name,
|
||||||
version: installedPlugin.manifest.version,
|
version: installedPlugin.manifest.version,
|
||||||
description: installedPlugin.manifest.description || '',
|
description: installedPlugin.manifest.description || '',
|
||||||
@@ -704,16 +713,23 @@ function PluginsPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 安装提示 */}
|
{/* 安装提示 */}
|
||||||
<Card className="border-blue-200 bg-blue-50 dark:bg-blue-950/20 dark:border-blue-900">
|
{restartNoticeVisible && (
|
||||||
<CardContent className="py-3">
|
<Card className="border-blue-200 bg-blue-50 dark:bg-blue-950/20 dark:border-blue-900">
|
||||||
<div className="flex items-center gap-2">
|
<CardContent className="py-3">
|
||||||
<Info className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
<div className="flex items-center gap-2">
|
||||||
安装、卸载或更新插件后,需要<span className="font-semibold">重启麦麦</span>才能使更改生效
|
<Info className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||||
</p>
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
</div>
|
安装、卸载或更新插件后,部分插件需要<span className="font-semibold">重启麦麦</span>才能生效
|
||||||
</CardContent>
|
</p>
|
||||||
</Card>
|
</div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={dismissRestartNotice}>
|
||||||
|
我知道了
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Git 状态警告 */}
|
{/* Git 状态警告 */}
|
||||||
{gitStatus && !gitStatus.installed && (
|
{gitStatus && !gitStatus.installed && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Code2,
|
Code2,
|
||||||
|
Copy,
|
||||||
FileCode2,
|
FileCode2,
|
||||||
FileText,
|
FileText,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useToast } from '@/hooks/use-toast'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +51,7 @@ function formatSize(size: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReasoningProcessPage() {
|
export function ReasoningProcessPage() {
|
||||||
|
const { toast } = useToast()
|
||||||
const [items, setItems] = useState<ReasoningPromptFile[]>([])
|
const [items, setItems] = useState<ReasoningPromptFile[]>([])
|
||||||
const [stages, setStages] = useState<string[]>([])
|
const [stages, setStages] = useState<string[]>([])
|
||||||
const [sessions, setSessions] = useState<string[]>([])
|
const [sessions, setSessions] = useState<string[]>([])
|
||||||
@@ -165,6 +168,31 @@ export function ReasoningProcessPage() {
|
|||||||
setPage(1)
|
setPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCopyPrompt() {
|
||||||
|
if (!textContent || contentLoading) {
|
||||||
|
toast({
|
||||||
|
title: '暂无可复制内容',
|
||||||
|
description: '请先选择一条包含 txt 的 prompt 记录',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(textContent)
|
||||||
|
toast({
|
||||||
|
title: '已复制完整 Prompt',
|
||||||
|
description: selected ? `${selected.stage}/${selected.session_id}/${selected.stem}` : undefined,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: '复制失败',
|
||||||
|
description: err instanceof Error ? err.message : '请手动选择文本复制',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-3 lg:p-4">
|
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-3 lg:p-4">
|
||||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||||||
@@ -328,6 +356,17 @@ export function ReasoningProcessPage() {
|
|||||||
</div>
|
</div>
|
||||||
{selected && (
|
{selected && (
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
onClick={handleCopyPrompt}
|
||||||
|
disabled={!selected.text_path || contentLoading || !textContent}
|
||||||
|
title="复制完整 Prompt"
|
||||||
|
>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
{selected.text_path && (
|
{selected.text_path && (
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<FileText className="h-3.5 w-3.5" />
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface HostApplication {
|
|||||||
export interface PluginManifest {
|
export interface PluginManifest {
|
||||||
/** 清单文件版本 */
|
/** 清单文件版本 */
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
/** Manifest 声明的插件唯一标识 */
|
||||||
|
id?: string
|
||||||
/** 插件名称 */
|
/** 插件名称 */
|
||||||
name: string
|
name: string
|
||||||
/** 插件版本 */
|
/** 插件版本 */
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
- EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d
|
- EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d
|
||||||
- PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6
|
- PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6
|
||||||
- MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1 # Docker 无法交互确认旧版升级迁移,默认跳过确认提示
|
- MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED=1 # Docker 无法交互确认旧版升级迁移,默认跳过确认提示
|
||||||
|
- MAIBOT_STATISTICS_REPORT_PATH=/MaiMBot/data/maibot_statistics.html # 统计数据输出到共享目录,首次运行可自动创建文件
|
||||||
# - EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d # 同意EULA
|
# - EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d # 同意EULA
|
||||||
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
|
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
|
||||||
ports:
|
ports:
|
||||||
@@ -20,7 +21,6 @@ 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配置文件
|
||||||
- ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
|
|
||||||
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
||||||
- ./data/MaiMBot/emoji:/data/emoji # 持久化表情包
|
- ./data/MaiMBot/emoji:/data/emoji # 持久化表情包
|
||||||
- ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
|
- ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ if [ ! -e "$ADAPTER_TARGET" ] && [ -d "$ADAPTER_TEMPLATE" ]; then
|
|||||||
cp -a "$ADAPTER_TEMPLATE" "$ADAPTER_TARGET"
|
cp -a "$ADAPTER_TEMPLATE" "$ADAPTER_TARGET"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
uv pip install --python "/MaiMBot/.venv/bin/python" --upgrade maibot-dashboard
|
||||||
|
|
||||||
exec /MaiMBot/.venv/bin/python bot.py "$@"
|
exec /MaiMBot/.venv/bin/python bot.py "$@"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ dependencies = [
|
|||||||
"jieba>=0.42.1",
|
"jieba>=0.42.1",
|
||||||
"json-repair>=0.47.6",
|
"json-repair>=0.47.6",
|
||||||
"maim-message>=0.6.2",
|
"maim-message>=0.6.2",
|
||||||
"maibot-dashboard>=1.0.8",
|
"maibot-dashboard>=1.0.10",
|
||||||
"maibot-plugin-sdk>=2.4.0",
|
"maibot-plugin-sdk>=2.4.0",
|
||||||
"matplotlib>=3.10.5",
|
"matplotlib>=3.10.5",
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|||||||
@@ -60,6 +60,60 @@ def test_load_prompt_with_category_falls_back_to_default_locale_root(tmp_path: P
|
|||||||
assert rendered == "你好,Mai"
|
assert rendered == "你好,Mai"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_prompt_prefers_custom_prompt_override(tmp_path: Path) -> None:
|
||||||
|
prompts_root = tmp_path / "prompts"
|
||||||
|
custom_prompts_root = tmp_path / "data" / "custom_prompts"
|
||||||
|
write_prompt(prompts_root, "zh-CN", "replyer", "Base {user_name}")
|
||||||
|
write_prompt(custom_prompts_root, "zh-CN", "replyer", "Custom {user_name}")
|
||||||
|
|
||||||
|
rendered = load_prompt(
|
||||||
|
"replyer",
|
||||||
|
locale="zh-CN",
|
||||||
|
prompts_root=prompts_root,
|
||||||
|
custom_prompts_root=custom_prompts_root,
|
||||||
|
user_name="Mai",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered == "Custom Mai"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_prompt_prefers_custom_prompt_requested_locale(tmp_path: Path) -> None:
|
||||||
|
prompts_root = tmp_path / "prompts"
|
||||||
|
custom_prompts_root = tmp_path / "data" / "custom_prompts"
|
||||||
|
write_prompt(prompts_root, "zh-CN", "replyer", "Base zh {user_name}")
|
||||||
|
write_prompt(prompts_root, "en-US", "replyer", "Base en {user_name}")
|
||||||
|
write_prompt(custom_prompts_root, "zh-CN", "replyer", "Custom zh {user_name}")
|
||||||
|
write_prompt(custom_prompts_root, "en-US", "replyer", "Custom en {user_name}")
|
||||||
|
|
||||||
|
rendered = load_prompt(
|
||||||
|
"replyer",
|
||||||
|
locale="en-US",
|
||||||
|
prompts_root=prompts_root,
|
||||||
|
custom_prompts_root=custom_prompts_root,
|
||||||
|
user_name="Mai",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered == "Custom en Mai"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_prompt_uses_requested_locale_source_before_default_custom(tmp_path: Path) -> None:
|
||||||
|
prompts_root = tmp_path / "prompts"
|
||||||
|
custom_prompts_root = tmp_path / "data" / "custom_prompts"
|
||||||
|
write_prompt(prompts_root, "zh-CN", "replyer", "Base zh {user_name}")
|
||||||
|
write_prompt(prompts_root, "en-US", "replyer", "Base en {user_name}")
|
||||||
|
write_prompt(custom_prompts_root, "zh-CN", "replyer", "Custom zh {user_name}")
|
||||||
|
|
||||||
|
rendered = load_prompt(
|
||||||
|
"replyer",
|
||||||
|
locale="en-US",
|
||||||
|
prompts_root=prompts_root,
|
||||||
|
custom_prompts_root=custom_prompts_root,
|
||||||
|
user_name="Mai",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered == "Base en Mai"
|
||||||
|
|
||||||
|
|
||||||
def test_load_prompt_strict_mode_raises_on_missing_placeholder(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_load_prompt_strict_mode_raises_on_missing_placeholder(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
prompts_root = tmp_path / "prompts"
|
prompts_root = tmp_path / "prompts"
|
||||||
write_prompt(prompts_root, "zh-CN", "replyer", "你好,{user_name},现在是 {current_time}")
|
write_prompt(prompts_root, "zh-CN", "replyer", "你好,{user_name},现在是 {current_time}")
|
||||||
|
|||||||
@@ -47,3 +47,17 @@ def test_installed_plugins_only_scan_plugins_dir_and_exclude_a_memorix(client: T
|
|||||||
assert ids == ["test.demo"]
|
assert ids == ["test.demo"]
|
||||||
assert "a-dawn.a-memorix" not in ids
|
assert "a-dawn.a-memorix" not in ids
|
||||||
assert all("/src/plugins/built_in/" not in plugin["path"] for plugin in payload["plugins"])
|
assert all("/src/plugins/built_in/" not in plugin["path"] for plugin in payload["plugins"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_installed_plugin_path_falls_back_to_manifest_id(client: TestClient):
|
||||||
|
plugin_path = support_module.resolve_installed_plugin_path("test.demo")
|
||||||
|
|
||||||
|
assert plugin_path is not None
|
||||||
|
assert plugin_path.name == "demo_plugin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_installed_plugin_path_accepts_manifest_id_case_mismatch(client: TestClient):
|
||||||
|
plugin_path = support_module.resolve_installed_plugin_path("Test.Demo")
|
||||||
|
|
||||||
|
assert plugin_path is not None
|
||||||
|
assert plugin_path.name == "demo_plugin"
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ tomlkit>=0.13.3
|
|||||||
typing-extensions
|
typing-extensions
|
||||||
uvicorn>=0.35.0
|
uvicorn>=0.35.0
|
||||||
watchfiles>=1.1.1
|
watchfiles>=1.1.1
|
||||||
maibot-dashboard>=1.0.8
|
maibot-dashboard>=1.0.10
|
||||||
@@ -2608,7 +2608,9 @@ class MetadataStore:
|
|||||||
Returns:
|
Returns:
|
||||||
段落列表
|
段落列表
|
||||||
"""
|
"""
|
||||||
return self.query("SELECT * FROM paragraphs WHERE source = ?", (source,))
|
cursor = self._conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM paragraphs WHERE source = ?", (source,))
|
||||||
|
return [self._row_to_dict(row, "paragraph") for row in cursor.fetchall()]
|
||||||
|
|
||||||
def get_all_sources(self) -> List[Dict[str, Any]]:
|
def get_all_sources(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -283,7 +283,13 @@ class PersonProfileService:
|
|||||||
logger.warning(f"解析人物别名失败: person_id={person_id}, err={e}")
|
logger.warning(f"解析人物别名失败: person_id={person_id}, err={e}")
|
||||||
return aliases, primary_name, memory_traits
|
return aliases, primary_name, memory_traits
|
||||||
|
|
||||||
def _collect_relation_evidence(self, aliases: List[str], limit: int = 30) -> List[Dict[str, Any]]:
|
def _collect_relation_evidence(
|
||||||
|
self,
|
||||||
|
aliases: List[str],
|
||||||
|
limit: int = 30,
|
||||||
|
*,
|
||||||
|
person_id: str = "",
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
relation_by_hash: Dict[str, Dict[str, Any]] = {}
|
relation_by_hash: Dict[str, Dict[str, Any]] = {}
|
||||||
for alias in aliases:
|
for alias in aliases:
|
||||||
for rel in self.metadata_store.get_relations(subject=alias, include_inactive=False):
|
for rel in self.metadata_store.get_relations(subject=alias, include_inactive=False):
|
||||||
@@ -296,6 +302,12 @@ class PersonProfileService:
|
|||||||
relation_by_hash[h] = rel
|
relation_by_hash[h] = rel
|
||||||
|
|
||||||
relations = list(relation_by_hash.values())
|
relations = list(relation_by_hash.values())
|
||||||
|
if person_id:
|
||||||
|
relations = [
|
||||||
|
rel
|
||||||
|
for rel in relations
|
||||||
|
if self._is_relation_bound_to_person(rel, person_id=person_id)
|
||||||
|
]
|
||||||
relations.sort(key=lambda item: float(item.get("confidence", 0.0)), reverse=True)
|
relations.sort(key=lambda item: float(item.get("confidence", 0.0)), reverse=True)
|
||||||
relations = relations[: max(1, int(limit))]
|
relations = relations[: max(1, int(limit))]
|
||||||
|
|
||||||
@@ -312,6 +324,38 @@ class PersonProfileService:
|
|||||||
)
|
)
|
||||||
return edges
|
return edges
|
||||||
|
|
||||||
|
def _is_relation_bound_to_person(
|
||||||
|
self,
|
||||||
|
relation: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
person_id: str,
|
||||||
|
) -> bool:
|
||||||
|
pid = str(person_id or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
metadata = self._metadata_dict(relation.get("metadata"))
|
||||||
|
if str(metadata.get("person_id", "") or "").strip() == pid:
|
||||||
|
return True
|
||||||
|
if pid in self._list_tokens(metadata.get("person_ids")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
source_paragraph = str(relation.get("source_paragraph", "") or "").strip()
|
||||||
|
if source_paragraph:
|
||||||
|
try:
|
||||||
|
paragraph = self.metadata_store.get_paragraph(source_paragraph)
|
||||||
|
except Exception:
|
||||||
|
paragraph = None
|
||||||
|
if isinstance(paragraph, dict):
|
||||||
|
payload = {
|
||||||
|
"hash": source_paragraph,
|
||||||
|
"source": str(paragraph.get("source", "") or ""),
|
||||||
|
"metadata": self._metadata_dict(paragraph.get("metadata")),
|
||||||
|
}
|
||||||
|
return self._is_evidence_bound_to_person(payload, person_id=pid)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _collect_person_fact_evidence(self, person_id: str, limit: int = 4) -> List[Dict[str, Any]]:
|
def _collect_person_fact_evidence(self, person_id: str, limit: int = 4) -> List[Dict[str, Any]]:
|
||||||
token = str(person_id or "").strip()
|
token = str(person_id or "").strip()
|
||||||
if not token:
|
if not token:
|
||||||
@@ -346,6 +390,42 @@ class PersonProfileService:
|
|||||||
)
|
)
|
||||||
return self._filter_stale_paragraph_evidence(evidence)
|
return self._filter_stale_paragraph_evidence(evidence)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _metadata_dict(value: Any) -> Dict[str, Any]:
|
||||||
|
return dict(value) if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _list_tokens(value: Any) -> List[str]:
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
return [str(item or "").strip() for item in value if str(item or "").strip()]
|
||||||
|
token = str(value or "").strip()
|
||||||
|
return [token] if token else []
|
||||||
|
|
||||||
|
def _is_evidence_bound_to_person(
|
||||||
|
self,
|
||||||
|
item: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
person_id: str,
|
||||||
|
) -> bool:
|
||||||
|
"""画像证据必须显式绑定到 person_id,避免别名全局召回串人。"""
|
||||||
|
pid = str(person_id or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
metadata = self._metadata_dict(item.get("metadata"))
|
||||||
|
source = str(item.get("source", "") or metadata.get("source", "") or "").strip()
|
||||||
|
if source == f"person_fact:{pid}":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if str(metadata.get("person_id", "") or "").strip() == pid:
|
||||||
|
return True
|
||||||
|
if pid in self._list_tokens(metadata.get("person_ids")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _source_type_from_source(source: str) -> str:
|
def _source_type_from_source(source: str) -> str:
|
||||||
token = str(source or "").strip()
|
token = str(source or "").strip()
|
||||||
@@ -360,7 +440,7 @@ class PersonProfileService:
|
|||||||
paragraph_hash: str,
|
paragraph_hash: str,
|
||||||
metadata: Dict[str, Any],
|
metadata: Dict[str, Any],
|
||||||
) -> Tuple[Dict[str, Any], str]:
|
) -> Tuple[Dict[str, Any], str]:
|
||||||
merged = dict(metadata or {})
|
merged = self._metadata_dict(metadata)
|
||||||
source = str(merged.get("source", "") or "").strip()
|
source = str(merged.get("source", "") or "").strip()
|
||||||
try:
|
try:
|
||||||
paragraph = self.metadata_store.get_paragraph(paragraph_hash)
|
paragraph = self.metadata_store.get_paragraph(paragraph_hash)
|
||||||
@@ -458,9 +538,11 @@ class PersonProfileService:
|
|||||||
"score": 0.0,
|
"score": 0.0,
|
||||||
"content": str(para.get("content", ""))[:180],
|
"content": str(para.get("content", ""))[:180],
|
||||||
"source": str(para.get("source", "") or ""),
|
"source": str(para.get("source", "") or ""),
|
||||||
"metadata": dict(para.get("metadata", {}) or {}),
|
"metadata": self._metadata_dict(para.get("metadata")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if not self._is_evidence_bound_to_person(fallback[-1], person_id=person_id):
|
||||||
|
fallback.pop()
|
||||||
return self._filter_stale_paragraph_evidence(fallback[:top_k])
|
return self._filter_stale_paragraph_evidence(fallback[:top_k])
|
||||||
|
|
||||||
per_alias_top_k = max(2, int(top_k / max(1, len(alias_queries))))
|
per_alias_top_k = max(2, int(top_k / max(1, len(alias_queries))))
|
||||||
@@ -483,21 +565,22 @@ class PersonProfileService:
|
|||||||
h = str(getattr(item, "hash_value", "") or "")
|
h = str(getattr(item, "hash_value", "") or "")
|
||||||
if not h or h in seen_hash:
|
if not h or h in seen_hash:
|
||||||
continue
|
continue
|
||||||
seen_hash.add(h)
|
|
||||||
metadata, source = self._enrich_paragraph_evidence_metadata(
|
metadata, source = self._enrich_paragraph_evidence_metadata(
|
||||||
h,
|
h,
|
||||||
dict(getattr(item, "metadata", {}) or {}),
|
self._metadata_dict(getattr(item, "metadata", {})),
|
||||||
)
|
|
||||||
evidence.append(
|
|
||||||
{
|
|
||||||
"hash": h,
|
|
||||||
"type": str(getattr(item, "result_type", "")),
|
|
||||||
"score": float(getattr(item, "score", 0.0) or 0.0),
|
|
||||||
"content": str(getattr(item, "content", "") or "")[:220],
|
|
||||||
"source": source,
|
|
||||||
"metadata": metadata,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
payload = {
|
||||||
|
"hash": h,
|
||||||
|
"type": str(getattr(item, "result_type", "")),
|
||||||
|
"score": float(getattr(item, "score", 0.0) or 0.0),
|
||||||
|
"content": str(getattr(item, "content", "") or "")[:220],
|
||||||
|
"source": source,
|
||||||
|
"metadata": metadata,
|
||||||
|
}
|
||||||
|
if not self._is_evidence_bound_to_person(payload, person_id=person_id):
|
||||||
|
continue
|
||||||
|
seen_hash.add(h)
|
||||||
|
evidence.append(payload)
|
||||||
evidence.sort(key=lambda x: x.get("score", 0.0), reverse=True)
|
evidence.sort(key=lambda x: x.get("score", 0.0), reverse=True)
|
||||||
return self._filter_stale_paragraph_evidence(evidence[:top_k])
|
return self._filter_stale_paragraph_evidence(evidence[:top_k])
|
||||||
|
|
||||||
@@ -640,7 +723,7 @@ class PersonProfileService:
|
|||||||
if not aliases and person_keyword:
|
if not aliases and person_keyword:
|
||||||
aliases = [person_keyword.strip()]
|
aliases = [person_keyword.strip()]
|
||||||
primary_name = person_keyword.strip()
|
primary_name = person_keyword.strip()
|
||||||
relation_edges = self._collect_relation_evidence(aliases, limit=max(10, top_k * 2))
|
relation_edges = self._collect_relation_evidence(aliases, limit=max(10, top_k * 2), person_id=pid)
|
||||||
vector_evidence = await self._collect_vector_evidence(aliases, top_k=max(4, top_k), person_id=pid)
|
vector_evidence = await self._collect_vector_evidence(aliases, top_k=max(4, top_k), person_id=pid)
|
||||||
|
|
||||||
evidence_ids = [
|
evidence_ids = [
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import traceback
|
|||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.services import llm_service as llm_api
|
from src.services import llm_service as llm_api
|
||||||
from src.services import message_service as message_api
|
from src.services import message_service as message_api
|
||||||
from src.config.config import global_config, model_config as host_model_config
|
from src.config.config import config_manager, global_config
|
||||||
from src.config.model_configs import TaskConfig
|
from src.config.model_configs import TaskConfig
|
||||||
|
|
||||||
from ..storage import (
|
from ..storage import (
|
||||||
@@ -150,36 +150,57 @@ class SummaryImporter:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _normalize_summary_model_selectors(self, raw_value: Any) -> List[str]:
|
def _normalize_summary_model_selectors(self, raw_value: Any) -> List[str]:
|
||||||
"""标准化 summarization.model_name 配置(vNext 仅接受字符串数组)。"""
|
"""标准化 summarization.model_name 配置。"""
|
||||||
if raw_value is None:
|
if raw_value is None:
|
||||||
return ["auto"]
|
return ["auto"]
|
||||||
if isinstance(raw_value, list):
|
if isinstance(raw_value, list):
|
||||||
selectors = [str(x).strip() for x in raw_value if str(x).strip()]
|
selectors = [str(x).strip() for x in raw_value if str(x).strip()]
|
||||||
return selectors or ["auto"]
|
return selectors or ["auto"]
|
||||||
|
if isinstance(raw_value, str):
|
||||||
|
selector = raw_value.strip()
|
||||||
|
if selector:
|
||||||
|
logger.warning("summarization.model_name 建议使用 List[str],当前字符串配置已兼容处理。")
|
||||||
|
return [selector]
|
||||||
|
return ["auto"]
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"summarization.model_name 在 vNext 必须为 List[str]。"
|
"summarization.model_name 必须为 List[str] 或 str。"
|
||||||
" 请执行 scripts/release_vnext_migrate.py migrate。"
|
" 请执行 scripts/release_vnext_migrate.py migrate。"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _pick_default_summary_task(self, available_tasks: Dict[str, TaskConfig]) -> Tuple[Optional[str], Optional[TaskConfig]]:
|
def _pick_default_summary_task(self, available_tasks: Dict[str, TaskConfig]) -> Tuple[Optional[str], Optional[TaskConfig]]:
|
||||||
"""
|
"""
|
||||||
选择总结默认任务,避免错误落到 embedding 任务。
|
选择总结默认任务,避免错误落到 embedding 任务。
|
||||||
优先级:memory > utils > planner;不再顺延到 replyer 或其他任务。
|
优先级:replyer > utils > planner > tool_use > 其他非 embedding。
|
||||||
"""
|
"""
|
||||||
preferred = ("memory", "utils", "planner")
|
preferred = ("replyer", "utils", "planner", "tool_use")
|
||||||
for name in preferred:
|
for name in preferred:
|
||||||
cfg = available_tasks.get(name)
|
cfg = available_tasks.get(name)
|
||||||
if cfg and cfg.model_list:
|
if cfg and cfg.model_list:
|
||||||
return name, cfg
|
return name, cfg
|
||||||
|
|
||||||
|
for name, cfg in available_tasks.items():
|
||||||
|
if name != "embedding" and cfg.model_list:
|
||||||
|
return name, cfg
|
||||||
|
|
||||||
|
for name, cfg in available_tasks.items():
|
||||||
|
if cfg.model_list:
|
||||||
|
return name, cfg
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def _resolve_summary_model_config(self) -> Optional[TaskConfig]:
|
@staticmethod
|
||||||
|
def _current_model_dict() -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return getattr(config_manager.get_model_config(), "models_dict", {}) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"读取当前模型字典失败: {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _resolve_summary_model_config(self) -> Optional[Tuple[str, TaskConfig]]:
|
||||||
"""
|
"""
|
||||||
解析 summarization.model_name 为 TaskConfig。
|
解析 summarization.model_name 为 (task_name, TaskConfig)。
|
||||||
支持:
|
支持:
|
||||||
- "auto"
|
- "auto"
|
||||||
- "memory"(任务名)
|
|
||||||
- "replyer"(任务名)
|
- "replyer"(任务名)
|
||||||
- "some-model-name"(具体模型名)
|
- "some-model-name"(具体模型名)
|
||||||
- ["utils:model1", "utils:model2", "replyer"](数组混合语法)
|
- ["utils:model1", "utils:model2", "replyer"](数组混合语法)
|
||||||
@@ -192,16 +213,18 @@ class SummaryImporter:
|
|||||||
# 避免默认值本身触发类型校验异常。
|
# 避免默认值本身触发类型校验异常。
|
||||||
raw_cfg = self.plugin_config.get("summarization", {}).get("model_name", ["auto"])
|
raw_cfg = self.plugin_config.get("summarization", {}).get("model_name", ["auto"])
|
||||||
selectors = self._normalize_summary_model_selectors(raw_cfg)
|
selectors = self._normalize_summary_model_selectors(raw_cfg)
|
||||||
_default_task_name, default_task_cfg = self._pick_default_summary_task(available_tasks)
|
default_task_name, default_task_cfg = self._pick_default_summary_task(available_tasks)
|
||||||
|
|
||||||
selected_models: List[str] = []
|
|
||||||
base_cfg: Optional[TaskConfig] = None
|
base_cfg: Optional[TaskConfig] = None
|
||||||
model_dict = getattr(host_model_config, "models_dict", {})
|
base_task_name: Optional[str] = None
|
||||||
|
model_dict = self._current_model_dict()
|
||||||
|
|
||||||
def _append_models(models: List[str]):
|
def _find_task_for_model(model_name: str) -> Tuple[Optional[str], Optional[TaskConfig]]:
|
||||||
for model_name in models:
|
for task_name, task_cfg in available_tasks.items():
|
||||||
if model_name and model_name not in selected_models:
|
task_models = [str(item).strip() for item in (getattr(task_cfg, "model_list", []) or []) if str(item).strip()]
|
||||||
selected_models.append(model_name)
|
if model_name in task_models:
|
||||||
|
return task_name, task_cfg
|
||||||
|
return None, None
|
||||||
|
|
||||||
for raw_selector in selectors:
|
for raw_selector in selectors:
|
||||||
selector = raw_selector.strip()
|
selector = raw_selector.strip()
|
||||||
@@ -210,9 +233,9 @@ class SummaryImporter:
|
|||||||
|
|
||||||
if selector.lower() == "auto":
|
if selector.lower() == "auto":
|
||||||
if default_task_cfg:
|
if default_task_cfg:
|
||||||
_append_models(default_task_cfg.model_list)
|
|
||||||
if base_cfg is None:
|
if base_cfg is None:
|
||||||
base_cfg = default_task_cfg
|
base_cfg = default_task_cfg
|
||||||
|
base_task_name = default_task_name
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ":" in selector:
|
if ":" in selector:
|
||||||
@@ -226,42 +249,60 @@ class SummaryImporter:
|
|||||||
|
|
||||||
if base_cfg is None:
|
if base_cfg is None:
|
||||||
base_cfg = task_cfg
|
base_cfg = task_cfg
|
||||||
|
base_task_name = task_name
|
||||||
|
|
||||||
if not model_name or model_name.lower() == "auto":
|
if not model_name or model_name.lower() == "auto":
|
||||||
_append_models(task_cfg.model_list)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if model_name in model_dict or model_name in task_cfg.model_list:
|
if model_name in task_cfg.model_list:
|
||||||
_append_models([model_name])
|
logger.info(
|
||||||
|
f"总结模型选择器 '{selector}' 已定位到任务 '{task_name}';"
|
||||||
|
"当前 LLM 服务按任务候选列表执行,不单独覆盖具体模型。"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"总结模型选择器 '{selector}' 的模型 '{model_name}' 不存在,已跳过")
|
logger.warning(f"总结模型选择器 '{selector}' 的模型 '{model_name}' 不在任务 '{task_name}' 中,已跳过")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
task_cfg = available_tasks.get(selector)
|
task_cfg = available_tasks.get(selector)
|
||||||
if task_cfg:
|
if task_cfg:
|
||||||
_append_models(task_cfg.model_list)
|
|
||||||
if base_cfg is None:
|
if base_cfg is None:
|
||||||
base_cfg = task_cfg
|
base_cfg = task_cfg
|
||||||
|
base_task_name = selector
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if selector in model_dict:
|
if selector in model_dict:
|
||||||
_append_models([selector])
|
task_name, task_cfg = _find_task_for_model(selector)
|
||||||
|
if task_name and task_cfg:
|
||||||
|
if base_cfg is None:
|
||||||
|
base_cfg = task_cfg
|
||||||
|
base_task_name = task_name
|
||||||
|
logger.info(
|
||||||
|
f"总结模型选择器 '{selector}' 已映射到任务 '{task_name}';"
|
||||||
|
"当前 LLM 服务按任务候选列表执行,不单独覆盖具体模型。"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
logger.warning(f"总结模型选择器 '{selector}' 未归属于任何任务,已跳过")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.warning(f"总结模型选择器 '{selector}' 无法识别,已跳过")
|
logger.warning(f"总结模型选择器 '{selector}' 无法识别,已跳过")
|
||||||
|
|
||||||
if not selected_models:
|
if base_cfg is None or not base_task_name:
|
||||||
if default_task_cfg:
|
if default_task_cfg:
|
||||||
_append_models(default_task_cfg.model_list)
|
|
||||||
if base_cfg is None:
|
if base_cfg is None:
|
||||||
base_cfg = default_task_cfg
|
base_cfg = default_task_cfg
|
||||||
|
base_task_name = default_task_name
|
||||||
|
else:
|
||||||
|
base_task_name, first_cfg = next(iter(available_tasks.items()))
|
||||||
|
if base_cfg is None:
|
||||||
|
base_cfg = first_cfg
|
||||||
|
|
||||||
if not selected_models:
|
if base_cfg is None or not base_task_name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
template_cfg = base_cfg or default_task_cfg or TaskConfig()
|
template_cfg = base_cfg
|
||||||
return TaskConfig(
|
task_name_to_use = base_task_name
|
||||||
model_list=selected_models,
|
return task_name_to_use, TaskConfig(
|
||||||
|
model_list=list(template_cfg.model_list),
|
||||||
max_tokens=template_cfg.max_tokens,
|
max_tokens=template_cfg.max_tokens,
|
||||||
temperature=template_cfg.temperature,
|
temperature=template_cfg.temperature,
|
||||||
slow_threshold=template_cfg.slow_threshold,
|
slow_threshold=template_cfg.slow_threshold,
|
||||||
@@ -331,12 +372,13 @@ class SummaryImporter:
|
|||||||
chat_history=chat_history_text
|
chat_history=chat_history_text
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config_to_use = self._resolve_summary_model_config()
|
resolved_model = self._resolve_summary_model_config()
|
||||||
if model_config_to_use is None:
|
if resolved_model is None:
|
||||||
return False, "未找到可用的总结模型配置"
|
return False, "未找到可用的总结模型配置"
|
||||||
task_name_to_use = llm_api.resolve_task_name_from_model_config(model_config_to_use)
|
task_name_to_use, model_config_to_use = resolved_model
|
||||||
|
|
||||||
logger.info(f"正在为流 {stream_id} 执行总结,消息条数: {len(messages)}")
|
logger.info(f"正在为流 {stream_id} 执行总结,消息条数: {len(messages)}")
|
||||||
|
logger.info(f"总结模型任务: {task_name_to_use}")
|
||||||
logger.info(f"总结模型候选列表: {model_config_to_use.model_list}")
|
logger.info(f"总结模型候选列表: {model_config_to_use.model_list}")
|
||||||
|
|
||||||
result = await llm_api.generate(
|
result = await llm_api.generate(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from os import getenv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from sqlmodel import col, select
|
from sqlmodel import col, select
|
||||||
@@ -26,6 +28,17 @@ from src.services.statistics_service import (
|
|||||||
|
|
||||||
logger = get_logger("maibot_statistic")
|
logger = get_logger("maibot_statistic")
|
||||||
|
|
||||||
|
STATISTICS_REPORT_PATH_ENV = "MAIBOT_STATISTICS_REPORT_PATH"
|
||||||
|
DEFAULT_STATISTICS_REPORT_PATH = "maibot_statistics.html"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_statistics_report_path(record_file_path: str | None = None) -> str:
|
||||||
|
if record_file_path:
|
||||||
|
return record_file_path
|
||||||
|
|
||||||
|
configured_path = getenv(STATISTICS_REPORT_PATH_ENV, "").strip()
|
||||||
|
return configured_path or DEFAULT_STATISTICS_REPORT_PATH
|
||||||
|
|
||||||
|
|
||||||
class StatPeriodData(TypedDict):
|
class StatPeriodData(TypedDict):
|
||||||
total_requests: int
|
total_requests: int
|
||||||
@@ -233,7 +246,7 @@ class StatisticOutputTask(AsyncTask):
|
|||||||
|
|
||||||
SEP_LINE = "-" * 84
|
SEP_LINE = "-" * 84
|
||||||
|
|
||||||
def __init__(self, record_file_path: str = "maibot_statistics.html"):
|
def __init__(self, record_file_path: str | None = None):
|
||||||
# 延迟300秒启动,运行间隔300秒
|
# 延迟300秒启动,运行间隔300秒
|
||||||
super().__init__(task_name="Statistics Data Output Task", wait_before_start=0, run_interval=300)
|
super().__init__(task_name="Statistics Data Output Task", wait_before_start=0, run_interval=300)
|
||||||
|
|
||||||
@@ -243,7 +256,7 @@ class StatisticOutputTask(AsyncTask):
|
|||||||
注:设计记录时间的目的是方便更新名称,使联系人/群聊名称保持最新
|
注:设计记录时间的目的是方便更新名称,使联系人/群聊名称保持最新
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.record_file_path: str = record_file_path
|
self.record_file_path: str = _resolve_statistics_report_path(record_file_path)
|
||||||
"""
|
"""
|
||||||
记录文件路径
|
记录文件路径
|
||||||
"""
|
"""
|
||||||
@@ -1730,7 +1743,11 @@ class StatisticOutputTask(AsyncTask):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(self.record_file_path, "w", encoding="utf-8") as f:
|
record_file = Path(self.record_file_path)
|
||||||
|
if record_file.parent != Path("."):
|
||||||
|
record_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(record_file, "w", encoding="utf-8") as f:
|
||||||
f.write(html_template)
|
f.write(html_template)
|
||||||
|
|
||||||
def _generate_chart_data(self, stat: StatPeriodMapping) -> dict[str, dict[str, object]]:
|
def _generate_chart_data(self, stat: StatPeriodMapping) -> dict[str, dict[str, object]]:
|
||||||
@@ -2431,7 +2448,7 @@ class StatisticOutputTask(AsyncTask):
|
|||||||
class AsyncStatisticOutputTask(AsyncTask):
|
class AsyncStatisticOutputTask(AsyncTask):
|
||||||
"""完全异步的统计输出任务 - 更高性能版本"""
|
"""完全异步的统计输出任务 - 更高性能版本"""
|
||||||
|
|
||||||
def __init__(self, record_file_path: str = "maibot_statistics.html"):
|
def __init__(self, record_file_path: str | None = None):
|
||||||
# 延迟0秒启动,运行间隔300秒
|
# 延迟0秒启动,运行间隔300秒
|
||||||
super().__init__(task_name="Async Statistics Data Output Task", wait_before_start=0, run_interval=300)
|
super().__init__(task_name="Async Statistics Data Output Task", wait_before_start=0, run_interval=300)
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ logger = logging.getLogger("maibot.prompt_i18n")
|
|||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
PROMPTS_ROOT = (PROJECT_ROOT / "prompts").resolve()
|
PROMPTS_ROOT = (PROJECT_ROOT / "prompts").resolve()
|
||||||
|
CUSTOM_PROMPTS_ROOT = (PROJECT_ROOT / "data" / "custom_prompts").resolve()
|
||||||
PROMPT_EXTENSIONS = (".prompt",)
|
PROMPT_EXTENSIONS = (".prompt",)
|
||||||
SAFE_SEGMENT_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
|
SAFE_SEGMENT_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
|
||||||
STRICT_ENV_KEYS = ("MAIBOT_PROMPT_I18N_STRICT", "MAIBOT_I18N_STRICT")
|
STRICT_ENV_KEYS = ("MAIBOT_PROMPT_I18N_STRICT", "MAIBOT_I18N_STRICT")
|
||||||
STRICT_ENV_VALUES = {"1", "true", "yes", "on"}
|
STRICT_ENV_VALUES = {"1", "true", "yes", "on"}
|
||||||
|
_PROMPT_CACHE_REVISION = 0
|
||||||
|
|
||||||
extract_prompt_placeholders = extract_placeholders
|
extract_prompt_placeholders = extract_placeholders
|
||||||
|
|
||||||
@@ -43,6 +45,17 @@ def get_prompts_root(prompts_root: Path | None = None) -> Path:
|
|||||||
return (prompts_root or PROMPTS_ROOT).resolve()
|
return (prompts_root or PROMPTS_ROOT).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def get_custom_prompts_root(
|
||||||
|
custom_prompts_root: Path | None = None,
|
||||||
|
prompts_root: Path | None = None,
|
||||||
|
) -> Path:
|
||||||
|
if custom_prompts_root is not None:
|
||||||
|
return custom_prompts_root.resolve()
|
||||||
|
if prompts_root is not None:
|
||||||
|
return (prompts_root.resolve().parent / "data" / "custom_prompts").resolve()
|
||||||
|
return CUSTOM_PROMPTS_ROOT
|
||||||
|
|
||||||
|
|
||||||
def normalize_prompt_name(name: str) -> str:
|
def normalize_prompt_name(name: str) -> str:
|
||||||
candidate_name = name.strip()
|
candidate_name = name.strip()
|
||||||
for suffix in PROMPT_EXTENSIONS:
|
for suffix in PROMPT_EXTENSIONS:
|
||||||
@@ -194,6 +207,28 @@ def _iter_locale_candidates(requested_locale: str) -> list[str]:
|
|||||||
return locale_candidates
|
return locale_candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_prompt_path_candidates(base_dir: Path, name: str, category: str | None = None) -> list[Path]:
|
||||||
|
candidates: list[Path] = []
|
||||||
|
for suffix in PROMPT_EXTENSIONS:
|
||||||
|
if category is not None:
|
||||||
|
candidates.append((base_dir / category / f"{name}{suffix}").resolve())
|
||||||
|
candidates.append((base_dir / f"{name}{suffix}").resolve())
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_custom_prompt_path(
|
||||||
|
name: str,
|
||||||
|
locale: str,
|
||||||
|
category: str | None,
|
||||||
|
custom_prompts_root: Path,
|
||||||
|
) -> Path | None:
|
||||||
|
custom_locale_dir = custom_prompts_root / locale
|
||||||
|
for candidate_path in _iter_prompt_path_candidates(custom_locale_dir, name, category):
|
||||||
|
if candidate_path.is_file():
|
||||||
|
return candidate_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def list_prompt_templates(locale: str | None = None, prompts_root: Path | None = None) -> dict[str, PromptTemplateInfo]:
|
def list_prompt_templates(locale: str | None = None, prompts_root: Path | None = None) -> dict[str, PromptTemplateInfo]:
|
||||||
resolved_prompts_root = get_prompts_root(prompts_root)
|
resolved_prompts_root = get_prompts_root(prompts_root)
|
||||||
requested_locale = normalize_locale(locale or get_locale())
|
requested_locale = normalize_locale(locale or get_locale())
|
||||||
@@ -206,15 +241,29 @@ def list_prompt_templates(locale: str | None = None, prompts_root: Path | None =
|
|||||||
|
|
||||||
|
|
||||||
def resolve_prompt_path(
|
def resolve_prompt_path(
|
||||||
name: str, locale: str | None = None, category: str | None = None, prompts_root: Path | None = None
|
name: str,
|
||||||
|
locale: str | None = None,
|
||||||
|
category: str | None = None,
|
||||||
|
prompts_root: Path | None = None,
|
||||||
|
custom_prompts_root: Path | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
resolved_prompts_root = get_prompts_root(prompts_root)
|
resolved_prompts_root = get_prompts_root(prompts_root)
|
||||||
|
resolved_custom_prompts_root = get_custom_prompts_root(custom_prompts_root, prompts_root)
|
||||||
normalized_name = normalize_prompt_name(name)
|
normalized_name = normalize_prompt_name(name)
|
||||||
normalized_category = normalize_prompt_category(category)
|
normalized_category = normalize_prompt_category(category)
|
||||||
requested_locale = normalize_locale(locale or get_locale())
|
requested_locale = normalize_locale(locale or get_locale())
|
||||||
|
|
||||||
if normalized_category is not None:
|
if normalized_category is not None:
|
||||||
for locale_candidate in _iter_locale_candidates(requested_locale):
|
for locale_candidate in _iter_locale_candidates(requested_locale):
|
||||||
|
custom_path = _resolve_custom_prompt_path(
|
||||||
|
normalized_name,
|
||||||
|
locale_candidate,
|
||||||
|
normalized_category,
|
||||||
|
resolved_custom_prompts_root,
|
||||||
|
)
|
||||||
|
if custom_path is not None:
|
||||||
|
return custom_path
|
||||||
|
|
||||||
base_dir = resolved_prompts_root / locale_candidate
|
base_dir = resolved_prompts_root / locale_candidate
|
||||||
for suffix in PROMPT_EXTENSIONS:
|
for suffix in PROMPT_EXTENSIONS:
|
||||||
candidate_path = (base_dir / normalized_category / f"{normalized_name}{suffix}").resolve()
|
candidate_path = (base_dir / normalized_category / f"{normalized_name}{suffix}").resolve()
|
||||||
@@ -226,9 +275,20 @@ def resolve_prompt_path(
|
|||||||
if fallback_path.is_file():
|
if fallback_path.is_file():
|
||||||
return fallback_path
|
return fallback_path
|
||||||
else:
|
else:
|
||||||
prompt_paths = list_prompt_templates(locale=requested_locale, prompts_root=resolved_prompts_root)
|
for locale_candidate in _iter_locale_candidates(requested_locale):
|
||||||
if normalized_name in prompt_paths:
|
custom_path = _resolve_custom_prompt_path(
|
||||||
return prompt_paths[normalized_name].path
|
normalized_name,
|
||||||
|
locale_candidate,
|
||||||
|
None,
|
||||||
|
resolved_custom_prompts_root,
|
||||||
|
)
|
||||||
|
if custom_path is not None:
|
||||||
|
return custom_path
|
||||||
|
|
||||||
|
base_dir = resolved_prompts_root / locale_candidate
|
||||||
|
for candidate_path in _iter_prompt_path_candidates(base_dir, normalized_name):
|
||||||
|
if candidate_path.is_file():
|
||||||
|
return candidate_path
|
||||||
|
|
||||||
raise FileNotFoundError(t("prompt.template_not_found", locale=requested_locale, name=normalized_name))
|
raise FileNotFoundError(t("prompt.template_not_found", locale=requested_locale, name=normalized_name))
|
||||||
|
|
||||||
@@ -263,13 +323,26 @@ def load_prompt(
|
|||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
category: str | None = None,
|
category: str | None = None,
|
||||||
prompts_root: Path | None = None,
|
prompts_root: Path | None = None,
|
||||||
|
custom_prompts_root: Path | None = None,
|
||||||
**kwargs: object,
|
**kwargs: object,
|
||||||
) -> str:
|
) -> str:
|
||||||
normalized_name = normalize_prompt_name(name)
|
normalized_name = normalize_prompt_name(name)
|
||||||
prompt_path = resolve_prompt_path(name=normalized_name, locale=locale, category=category, prompts_root=prompts_root)
|
prompt_path = resolve_prompt_path(
|
||||||
|
name=normalized_name,
|
||||||
|
locale=locale,
|
||||||
|
category=category,
|
||||||
|
prompts_root=prompts_root,
|
||||||
|
custom_prompts_root=custom_prompts_root,
|
||||||
|
)
|
||||||
template = _read_prompt_template(prompt_path)
|
template = _read_prompt_template(prompt_path)
|
||||||
return _format_prompt_template(normalized_name, template, **kwargs)
|
return _format_prompt_template(normalized_name, template, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def clear_prompt_cache() -> None:
|
def clear_prompt_cache() -> None:
|
||||||
|
global _PROMPT_CACHE_REVISION
|
||||||
|
_PROMPT_CACHE_REVISION += 1
|
||||||
_read_prompt_template.cache_clear()
|
_read_prompt_template.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompt_cache_revision() -> int:
|
||||||
|
return _PROMPT_CACHE_REVISION
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
|
|||||||
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
|
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
|
||||||
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
|
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
|
||||||
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
|
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
|
||||||
MMC_VERSION: str = "1.0.0-pre.15"
|
MMC_VERSION: str = "1.0.0-pre.16"
|
||||||
CONFIG_VERSION: str = "8.10.15"
|
CONFIG_VERSION: str = "8.10.15"
|
||||||
MODEL_CONFIG_VERSION: str = "1.16.1"
|
MODEL_CONFIG_VERSION: str = "1.16.1"
|
||||||
|
|
||||||
|
|||||||
@@ -388,7 +388,6 @@ class EmojiManager:
|
|||||||
if existing_record := session.exec(statement).first():
|
if existing_record := session.exec(statement).first():
|
||||||
existing_record.full_path = str(emoji.full_path)
|
existing_record.full_path = str(emoji.full_path)
|
||||||
existing_record.no_file_flag = False
|
existing_record.no_file_flag = False
|
||||||
existing_record.is_banned = False
|
|
||||||
existing_record.last_used_time = datetime.now()
|
existing_record.last_used_time = datetime.now()
|
||||||
existing_record.query_count += 1
|
existing_record.query_count += 1
|
||||||
session.add(existing_record)
|
session.add(existing_record)
|
||||||
@@ -473,7 +472,6 @@ class EmojiManager:
|
|||||||
image_record.full_path = str(new_emoji.full_path)
|
image_record.full_path = str(new_emoji.full_path)
|
||||||
image_record.description = new_emoji.description
|
image_record.description = new_emoji.description
|
||||||
image_record.no_file_flag = False
|
image_record.no_file_flag = False
|
||||||
image_record.is_banned = False
|
|
||||||
session.add(image_record)
|
session.add(image_record)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"Update cached emoji description failed: {exc}")
|
logger.error(f"Update cached emoji description failed: {exc}")
|
||||||
@@ -531,6 +529,9 @@ class EmojiManager:
|
|||||||
statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1)
|
statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1)
|
||||||
existing_record = session.exec(statement).first()
|
existing_record = session.exec(statement).first()
|
||||||
if existing_record:
|
if existing_record:
|
||||||
|
if existing_record.is_banned:
|
||||||
|
logger.info(f"[register_emoji] Emoji is banned, skipping: {emoji.file_hash}")
|
||||||
|
return "skipped"
|
||||||
if existing_record.is_registered and _is_available_emoji_record(existing_record):
|
if existing_record.is_registered and _is_available_emoji_record(existing_record):
|
||||||
# logger.info(f"[register_emoji] Emoji already registered, skipping: {emoji.file_hash}")
|
# logger.info(f"[register_emoji] Emoji already registered, skipping: {emoji.file_hash}")
|
||||||
return "skipped"
|
return "skipped"
|
||||||
@@ -1085,6 +1086,10 @@ class EmojiManager:
|
|||||||
return "failed"
|
return "failed"
|
||||||
|
|
||||||
if existing_record is not None:
|
if existing_record is not None:
|
||||||
|
if existing_record.is_banned:
|
||||||
|
logger.info(f"[register_emoji] Emoji is banned, skipping: {target_emoji.file_name}")
|
||||||
|
return "skipped"
|
||||||
|
|
||||||
if existing_record.is_registered and _is_available_emoji_record(existing_record):
|
if existing_record.is_registered and _is_available_emoji_record(existing_record):
|
||||||
logger.info(f"[register_emoji] Emoji already registered, skipping: {target_emoji.file_name}")
|
logger.info(f"[register_emoji] Emoji already registered, skipping: {target_emoji.file_name}")
|
||||||
return "skipped"
|
return "skipped"
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ async def handle_tool(
|
|||||||
)
|
)
|
||||||
return tool_ctx.build_success_result(
|
return tool_ctx.build_success_result(
|
||||||
invocation.tool_name,
|
invocation.tool_name,
|
||||||
"回复已生成并发送。",
|
f'已生成并发送回复"{combined_reply_text}"\n发送对象:{target_user_name}',
|
||||||
structured_content={
|
structured_content={
|
||||||
"msg_id": target_message_id,
|
"msg_id": target_message_id,
|
||||||
"set_quote": set_quote,
|
"set_quote": set_quote,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from rich.console import RenderableType
|
|||||||
from src.common.data_models.llm_service_data_models import LLMGenerationOptions
|
from src.common.data_models.llm_service_data_models import LLMGenerationOptions
|
||||||
from src.common.i18n import get_locale
|
from src.common.i18n import get_locale
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.common.prompt_i18n import load_prompt
|
from src.common.prompt_i18n import get_prompt_cache_revision, load_prompt
|
||||||
from src.common.utils.utils_config import ChatConfigUtils
|
from src.common.utils.utils_config import ChatConfigUtils
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
from src.core.tooling import ToolAvailabilityContext, ToolRegistry
|
from src.core.tooling import ToolAvailabilityContext, ToolRegistry
|
||||||
@@ -219,6 +219,7 @@ class MaisakaChatLoopService:
|
|||||||
self._interrupt_flag: asyncio.Event | None = None
|
self._interrupt_flag: asyncio.Event | None = None
|
||||||
self._tool_registry: ToolRegistry | None = None
|
self._tool_registry: ToolRegistry | None = None
|
||||||
self._prompts_loaded = chat_system_prompt is not None
|
self._prompts_loaded = chat_system_prompt is not None
|
||||||
|
self._prompt_cache_revision = get_prompt_cache_revision()
|
||||||
self._prompt_load_lock = asyncio.Lock()
|
self._prompt_load_lock = asyncio.Lock()
|
||||||
self._personality_prompt = self._build_personality_prompt()
|
self._personality_prompt = self._build_personality_prompt()
|
||||||
if chat_system_prompt is None:
|
if chat_system_prompt is None:
|
||||||
@@ -354,6 +355,7 @@ class MaisakaChatLoopService:
|
|||||||
self._chat_system_prompt = f"{self._personality_prompt}\n\nYou are a helpful AI assistant."
|
self._chat_system_prompt = f"{self._personality_prompt}\n\nYou are a helpful AI assistant."
|
||||||
|
|
||||||
self._prompts_loaded = True
|
self._prompts_loaded = True
|
||||||
|
self._prompt_cache_revision = get_prompt_cache_revision()
|
||||||
|
|
||||||
def build_prompt_template_context(self, tools_section: str = "") -> dict[str, str]:
|
def build_prompt_template_context(self, tools_section: str = "") -> dict[str, str]:
|
||||||
"""构造 Maisaka prompt 模板的公共渲染参数。"""
|
"""构造 Maisaka prompt 模板的公共渲染参数。"""
|
||||||
@@ -519,7 +521,7 @@ class MaisakaChatLoopService:
|
|||||||
ChatResponse: 本轮规划器返回结果。
|
ChatResponse: 本轮规划器返回结果。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._prompts_loaded:
|
if not self._prompts_loaded or self._prompt_cache_revision != get_prompt_cache_revision():
|
||||||
await self.ensure_chat_prompt_loaded()
|
await self.ensure_chat_prompt_loaded()
|
||||||
enable_visual_message = self._resolve_enable_visual_message(request_kind)
|
enable_visual_message = self._resolve_enable_visual_message(request_kind)
|
||||||
selected_history, selection_reason = self.select_llm_context_messages(
|
selected_history, selection_reason = self.select_llm_context_messages(
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ logger = get_logger("webui.app")
|
|||||||
|
|
||||||
_DASHBOARD_PACKAGE_NAME = "maibot-dashboard"
|
_DASHBOARD_PACKAGE_NAME = "maibot-dashboard"
|
||||||
_LOCAL_DASHBOARD_ENV = "MAIBOT_WEBUI_USE_LOCAL_DASHBOARD"
|
_LOCAL_DASHBOARD_ENV = "MAIBOT_WEBUI_USE_LOCAL_DASHBOARD"
|
||||||
|
_STATISTICS_REPORT_PATH_ENV = "MAIBOT_STATISTICS_REPORT_PATH"
|
||||||
|
_DEFAULT_STATISTICS_REPORT_PATH = "maibot_statistics.html"
|
||||||
_MANUAL_INSTALL_COMMAND = f"pip install {_DASHBOARD_PACKAGE_NAME}"
|
_MANUAL_INSTALL_COMMAND = f"pip install {_DASHBOARD_PACKAGE_NAME}"
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +40,15 @@ def _get_project_root() -> Path:
|
|||||||
return Path(__file__).resolve().parents[2]
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_statistics_report_path() -> Path:
|
||||||
|
configured_path = getenv(_STATISTICS_REPORT_PATH_ENV, "").strip()
|
||||||
|
report_path = Path(configured_path or _DEFAULT_STATISTICS_REPORT_PATH)
|
||||||
|
if report_path.is_absolute():
|
||||||
|
return report_path.resolve()
|
||||||
|
|
||||||
|
return (_get_project_root() / report_path).resolve()
|
||||||
|
|
||||||
|
|
||||||
def _is_local_dashboard_enabled() -> bool:
|
def _is_local_dashboard_enabled() -> bool:
|
||||||
return getenv(_LOCAL_DASHBOARD_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
|
return getenv(_LOCAL_DASHBOARD_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
@@ -187,7 +198,7 @@ def _setup_static_files(app: FastAPI):
|
|||||||
|
|
||||||
@app.get("/maibot_statistics.html", include_in_schema=False)
|
@app.get("/maibot_statistics.html", include_in_schema=False)
|
||||||
async def serve_statistics_report():
|
async def serve_statistics_report():
|
||||||
report_path = (_get_project_root() / "maibot_statistics.html").resolve()
|
report_path = _resolve_statistics_report_path()
|
||||||
if not report_path.exists() or not report_path.is_file():
|
if not report_path.exists() or not report_path.is_file():
|
||||||
raise HTTPException(status_code=404, detail=t("core.not_found"))
|
raise HTTPException(status_code=404, detail=t("core.not_found"))
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field
|
|||||||
import tomlkit
|
import tomlkit
|
||||||
|
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.common.prompt_i18n import list_prompt_templates
|
from src.common.prompt_i18n import clear_prompt_cache, list_prompt_templates
|
||||||
from src.config.config import CONFIG_DIR, PROJECT_ROOT, Config, ModelConfig
|
from src.config.config import CONFIG_DIR, PROJECT_ROOT, Config, ModelConfig
|
||||||
from src.config.config_base import AttributeData, ConfigBase
|
from src.config.config_base import AttributeData, ConfigBase
|
||||||
from src.config.model_configs import (
|
from src.config.model_configs import (
|
||||||
@@ -323,6 +323,7 @@ async def update_prompt_file(language: str, filename: str, content: PromptConten
|
|||||||
try:
|
try:
|
||||||
custom_prompt_path.parent.mkdir(parents=True, exist_ok=True)
|
custom_prompt_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
custom_prompt_path.write_text(content, encoding="utf-8", newline="\n")
|
custom_prompt_path.write_text(content, encoding="utf-8", newline="\n")
|
||||||
|
clear_prompt_cache()
|
||||||
return PromptFileResponse(language=language, filename=filename, content=content, customized=True)
|
return PromptFileResponse(language=language, filename=filename, content=content, customized=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存 Prompt 文件失败: {prompt_path} {e}", exc_info=True)
|
logger.error(f"保存 Prompt 文件失败: {prompt_path} {e}", exc_info=True)
|
||||||
@@ -341,6 +342,7 @@ async def reset_prompt_file(language: str, filename: str):
|
|||||||
try:
|
try:
|
||||||
if custom_prompt_path.exists():
|
if custom_prompt_path.exists():
|
||||||
custom_prompt_path.unlink()
|
custom_prompt_path.unlink()
|
||||||
|
clear_prompt_cache()
|
||||||
content = prompt_path.read_text(encoding="utf-8")
|
content = prompt_path.read_text(encoding="utf-8")
|
||||||
return PromptFileResponse(language=language, filename=filename, content=content, customized=False)
|
return PromptFileResponse(language=language, filename=filename, content=content, customized=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ def resolve_installed_plugin_path(plugin_id: str) -> Optional[Path]:
|
|||||||
return _resolve_safe_plugin_directory(new_format_path, plugins_dir, strict=True)
|
return _resolve_safe_plugin_directory(new_format_path, plugins_dir, strict=True)
|
||||||
if old_format_path.exists():
|
if old_format_path.exists():
|
||||||
return _resolve_safe_plugin_directory(old_format_path, plugins_dir, strict=True)
|
return _resolve_safe_plugin_directory(old_format_path, plugins_dir, strict=True)
|
||||||
return None
|
return find_plugin_path_by_id(plugin_id)
|
||||||
|
|
||||||
|
|
||||||
def parse_repository_url(repository_url: str) -> Tuple[str, str, str]:
|
def parse_repository_url(repository_url: str) -> Tuple[str, str, str]:
|
||||||
@@ -256,11 +256,29 @@ def iter_plugin_directories() -> List[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def find_plugin_path_by_id(plugin_id: str) -> Optional[Path]:
|
def find_plugin_path_by_id(plugin_id: str) -> Optional[Path]:
|
||||||
|
casefold_matched_path: Optional[Path] = None
|
||||||
|
normalized_plugin_id = plugin_id.casefold()
|
||||||
|
|
||||||
for plugin_path in iter_plugin_directories():
|
for plugin_path in iter_plugin_directories():
|
||||||
manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
|
manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
|
||||||
manifest = load_manifest_json(manifest_path)
|
manifest = load_manifest_json(manifest_path)
|
||||||
if manifest is not None and (manifest.get("id") == plugin_id or plugin_path.name == plugin_id):
|
if manifest is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
manifest_id = str(manifest.get("id", ""))
|
||||||
|
if manifest_id == plugin_id or plugin_path.name == plugin_id:
|
||||||
return plugin_path
|
return plugin_path
|
||||||
|
|
||||||
|
if (
|
||||||
|
casefold_matched_path is None
|
||||||
|
and (manifest_id.casefold() == normalized_plugin_id or plugin_path.name.casefold() == normalized_plugin_id)
|
||||||
|
):
|
||||||
|
casefold_matched_path = plugin_path
|
||||||
|
|
||||||
|
if casefold_matched_path is not None:
|
||||||
|
logger.warning(f"插件 ID 大小写不一致,已按大小写不敏感匹配: {plugin_id} -> {casefold_matched_path}")
|
||||||
|
return casefold_matched_path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +287,9 @@ def backup_file(file_path: Path, action: str, move_file: bool = False) -> Option
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
backup_name = f"{file_path.name}.{action}.{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
backup_name = f"{file_path.name}.{action}.{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
backup_path = file_path.parent / backup_name
|
backup_dir = file_path.parent / "config_back"
|
||||||
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup_path = backup_dir / backup_name
|
||||||
if move_file:
|
if move_file:
|
||||||
shutil.move(file_path, backup_path)
|
shutil.move(file_path, backup_path)
|
||||||
else:
|
else:
|
||||||
|
|||||||
8
uv.lock
generated
8
uv.lock
generated
@@ -1511,7 +1511,7 @@ requires-dist = [
|
|||||||
{ name = "httpx", extras = ["socks"] },
|
{ name = "httpx", extras = ["socks"] },
|
||||||
{ name = "jieba", specifier = ">=0.42.1" },
|
{ name = "jieba", specifier = ">=0.42.1" },
|
||||||
{ name = "json-repair", specifier = ">=0.47.6" },
|
{ name = "json-repair", specifier = ">=0.47.6" },
|
||||||
{ name = "maibot-dashboard", specifier = ">=1.0.8" },
|
{ name = "maibot-dashboard", specifier = ">=1.0.9" },
|
||||||
{ name = "maibot-plugin-sdk", specifier = ">=2.4.0" },
|
{ name = "maibot-plugin-sdk", specifier = ">=2.4.0" },
|
||||||
{ name = "maim-message", specifier = ">=0.6.2" },
|
{ name = "maim-message", specifier = ">=0.6.2" },
|
||||||
{ name = "matplotlib", specifier = ">=3.10.5" },
|
{ name = "matplotlib", specifier = ">=3.10.5" },
|
||||||
@@ -1549,11 +1549,11 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maibot-dashboard"
|
name = "maibot-dashboard"
|
||||||
version = "1.0.8"
|
version = "1.0.9"
|
||||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/9f/e59b1a6299cc4f8c9ac16c7c2774581220fdd27227ac9c2fdfb947dfc2f5/maibot_dashboard-1.0.8.tar.gz", hash = "sha256:a47309072d8154905738d02ccad17a543d5159a1e62ca87076ac4dce39e6c922", size = 2496374, upload-time = "2026-05-07T13:58:39.386Z" }
|
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/5b/e90896cbdddc89ec5586873de07a3d70c0107e4dc76db8666a0c0fde6ae8/maibot_dashboard-1.0.9.tar.gz", hash = "sha256:0e5c00be021419686105238cded501024f0383a3815bd85f9a1e747f3f04d0cd", size = 2496957, upload-time = "2026-05-07T18:37:51.291Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/60/fde671bf332133f1403673096eefcd49f36133141a6b9229e72c2588b221/maibot_dashboard-1.0.8-py3-none-any.whl", hash = "sha256:39da973fed56f1491245109615d81ea79add859467798af92d4ace7d8a5d7557", size = 2563243, upload-time = "2026-05-07T13:58:37.868Z" },
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/27/ab227a84e55356039004a375e78031e5e8aaf4192e11908a568498816d5e/maibot_dashboard-1.0.9-py3-none-any.whl", hash = "sha256:197b26c5c3d0e6ba1238b91d12c88e57db71c65303cc602fcccdca84ce4db582", size = 2563281, upload-time = "2026-05-07T18:37:49.648Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user