fix:修复麦麦观察的一些不一致问题(混乱!?
This commit is contained in:
@@ -175,9 +175,12 @@ export interface PlannerFinalizedEvent {
|
|||||||
request: MaisakaRequestBlock | null
|
request: MaisakaRequestBlock | null
|
||||||
planner: MaisakaPlannerBlock | null
|
planner: MaisakaPlannerBlock | null
|
||||||
tools: MaisakaFinalizedToolResult[]
|
tools: MaisakaFinalizedToolResult[]
|
||||||
|
interrupted?: boolean
|
||||||
final_state: {
|
final_state: {
|
||||||
time_records: Record<string, number>
|
time_records: Record<string, number>
|
||||||
agent_state: string
|
agent_state: string
|
||||||
|
end_reason?: string
|
||||||
|
end_detail?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +189,8 @@ export interface CycleEndEvent {
|
|||||||
cycle_id: number
|
cycle_id: number
|
||||||
time_records: Record<string, number>
|
time_records: Record<string, number>
|
||||||
agent_state: string
|
agent_state: string
|
||||||
|
end_reason?: string
|
||||||
|
end_detail?: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export interface LocalCacheStats {
|
|||||||
export interface LocalCacheCleanupResult {
|
export interface LocalCacheCleanupResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
message: string
|
message: string
|
||||||
target: 'images' | 'emoji' | 'logs'
|
target: 'images' | 'emoji' | 'log_files' | 'database_logs'
|
||||||
removed_files: number
|
removed_files: number
|
||||||
removed_bytes: number
|
removed_bytes: number
|
||||||
removed_records: number
|
removed_records: number
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Bot,
|
Bot,
|
||||||
Brain,
|
Brain,
|
||||||
@@ -315,6 +316,38 @@ function openPromptHtml(uri: string) {
|
|||||||
window.open(normalized, '_blank', 'noopener,noreferrer')
|
window.open(normalized, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlannerInterrupted(data: PlannerFinalizedEvent) {
|
||||||
|
const content = data.planner?.content?.trim() ?? ''
|
||||||
|
return data.interrupted === true || (
|
||||||
|
content.startsWith('Planner ') &&
|
||||||
|
data.planner?.prompt_tokens === 0 &&
|
||||||
|
data.planner?.completion_tokens === 0 &&
|
||||||
|
data.planner?.tool_calls.length === 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlannerInterruptedCard({ data }: { data: PlannerFinalizedEvent }) {
|
||||||
|
const planner = data.planner
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-amber-500/35 bg-amber-500/5 px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<AlertCircle className="h-4 w-4 shrink-0 text-amber-500" />
|
||||||
|
<span className="font-medium">Planner 被新消息打断</span>
|
||||||
|
<Badge variant="outline" className="ml-auto text-[10px]">
|
||||||
|
#{data.cycle_id}
|
||||||
|
</Badge>
|
||||||
|
{planner && planner.duration_ms > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">{formatMs(planner.duration_ms)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{planner?.content || '收到新消息,已停止当前思考并准备重新决策。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function PlannerResponseCard({ data }: { data: PlannerResponseEvent }) {
|
function PlannerResponseCard({ data }: { data: PlannerResponseEvent }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -501,26 +534,62 @@ function ToolExecutionCard({ data }: { data: ToolExecutionEvent }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCycleEndReasonText(data: CycleEndEvent) {
|
||||||
|
const reason = data.end_reason ?? ''
|
||||||
|
const detail = data.end_detail?.trim()
|
||||||
|
|
||||||
|
if (detail) {
|
||||||
|
return detail
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason === 'finish') return 'Planner 调用 finish,结束本轮思考并等待新消息。'
|
||||||
|
if (reason === 'timing_no_reply') return 'Timing Gate 选择 no_reply,本轮不会进入 Planner。'
|
||||||
|
if (reason === 'max_rounds') return '已达到内部思考轮次上限,本轮处理结束。'
|
||||||
|
if (reason === 'planner_interrupted') return 'Planner 被新消息打断,当前轮结束。'
|
||||||
|
if (reason.startsWith('tool_pause:')) return `工具 ${reason.slice('tool_pause:'.length)} 要求暂停当前思考循环。`
|
||||||
|
if (reason === 'tool_pause') return '工具要求暂停当前思考循环。'
|
||||||
|
if (reason === 'empty_planner_response') return 'Planner 没有返回文本或工具调用,本轮思考结束。'
|
||||||
|
if (reason === 'tool_continue') return 'Planner 工具执行完成,继续下一轮内部思考。'
|
||||||
|
return '本轮思考完成。'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCycleEndReasonLabel(data: CycleEndEvent) {
|
||||||
|
const reason = data.end_reason ?? ''
|
||||||
|
|
||||||
|
if (reason === 'finish') return 'finish 结束'
|
||||||
|
if (reason === 'timing_no_reply') return 'no_reply 结束'
|
||||||
|
if (reason === 'max_rounds') return '轮次上限'
|
||||||
|
if (reason === 'planner_interrupted') return 'Planner 打断'
|
||||||
|
if (reason.startsWith('tool_pause:')) return '工具暂停'
|
||||||
|
if (reason === 'tool_pause') return '工具暂停'
|
||||||
|
if (reason === 'empty_planner_response') return '空响应'
|
||||||
|
if (reason === 'tool_continue') return '继续下一轮'
|
||||||
|
return '循环结束'
|
||||||
|
}
|
||||||
|
|
||||||
function CycleEndCard({ data }: { data: CycleEndEvent }) {
|
function CycleEndCard({ data }: { data: CycleEndEvent }) {
|
||||||
const totalTime = Object.values(data.time_records).reduce((a, b) => a + b, 0)
|
const totalTime = Object.values(data.time_records).reduce((a, b) => a + b, 0)
|
||||||
return (
|
return (
|
||||||
<div className="my-1 flex items-center gap-3">
|
<div className="my-1 space-y-1.5">
|
||||||
<Separator className="flex-1" />
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 rounded-full border bg-background px-3 py-1">
|
<Separator className="flex-1" />
|
||||||
<CircleDot className="h-3.5 w-3.5 text-slate-500" />
|
<div className="flex items-center gap-2 rounded-full border bg-background px-3 py-1">
|
||||||
<span className="text-xs text-muted-foreground">循环结束</span>
|
<CircleDot className="h-3.5 w-3.5 text-slate-500" />
|
||||||
<Badge variant="outline" className="text-[10px]">
|
<span className="text-xs text-muted-foreground">{getCycleEndReasonLabel(data)}</span>
|
||||||
#{data.cycle_id}
|
<Badge variant="outline" className="text-[10px]">
|
||||||
</Badge>
|
#{data.cycle_id}
|
||||||
<span className="text-[10px] text-muted-foreground">{formatMs(totalTime * 1000)}</span>
|
</Badge>
|
||||||
<Badge
|
<span className="text-[10px] text-muted-foreground">{formatMs(totalTime * 1000)}</span>
|
||||||
variant={data.agent_state === 'running' ? 'default' : 'secondary'}
|
<Badge
|
||||||
className="text-[10px]"
|
variant={data.agent_state === 'running' ? 'default' : 'secondary'}
|
||||||
>
|
className="text-[10px]"
|
||||||
{data.agent_state}
|
>
|
||||||
</Badge>
|
{data.agent_state}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Separator className="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
<Separator className="flex-1" />
|
<p className="text-center text-xs text-muted-foreground">{getCycleEndReasonText(data)}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -648,7 +717,10 @@ function TimelineEventRenderer({
|
|||||||
case 'planner.response':
|
case 'planner.response':
|
||||||
return <PlannerResponseCard data={entry.data as PlannerResponseEvent} />
|
return <PlannerResponseCard data={entry.data as PlannerResponseEvent} />
|
||||||
case 'planner.finalized':
|
case 'planner.finalized':
|
||||||
if ((entry.data as PlannerFinalizedEvent).timing_gate?.result?.action !== 'continue') {
|
if (isPlannerInterrupted(entry.data as PlannerFinalizedEvent)) {
|
||||||
|
return <PlannerInterruptedCard data={entry.data as PlannerFinalizedEvent} />
|
||||||
|
}
|
||||||
|
if ((entry.data as PlannerFinalizedEvent).timing_gate?.result?.action === 'no_reply') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -856,23 +928,32 @@ export function MaisakaMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
const continuedTimingGateCycles = new Set<string>()
|
const noReplyTimingGateCycles = new Set<string>()
|
||||||
const stoppedTimingGateCycles = new Set<string>()
|
|
||||||
|
|
||||||
return timeline.map((entry) => {
|
return timeline.map((entry) => {
|
||||||
if (entry.type === 'timing_gate.result') {
|
if (entry.type === 'timing_gate.result') {
|
||||||
const data = entry.data as TimingGateResultEvent
|
const data = entry.data as TimingGateResultEvent
|
||||||
if (data.action === 'continue') {
|
if (data.action === 'no_reply') {
|
||||||
continuedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id))
|
noReplyTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id))
|
||||||
} else {
|
|
||||||
stoppedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'planner.response' || entry.type === 'planner.finalized') {
|
if (entry.type === 'planner.response' || entry.type === 'planner.finalized') {
|
||||||
const data = entry.data as PlannerResponseEvent | PlannerFinalizedEvent
|
const data = entry.data as PlannerResponseEvent | PlannerFinalizedEvent
|
||||||
const cycleKey = buildCycleKey(data.session_id, data.cycle_id)
|
const cycleKey = buildCycleKey(data.session_id, data.cycle_id)
|
||||||
if (stoppedTimingGateCycles.has(cycleKey) || !continuedTimingGateCycles.has(cycleKey)) {
|
if (entry.type === 'planner.finalized' && isPlannerInterrupted(data as PlannerFinalizedEvent)) {
|
||||||
|
const rendered = <TimelineEventRenderer entry={entry} showCycleMarkers={showCycleMarkers} />
|
||||||
|
if (!rendered) return null
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
className="animate-in fade-in-0 slide-in-from-bottom-2 duration-300"
|
||||||
|
>
|
||||||
|
{rendered}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (noReplyTimingGateCycles.has(cycleKey)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -897,3 +978,4 @@ export function MaisakaMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -389,6 +389,14 @@ function updateSessionInfo(event: MaisakaMonitorEvent, sessionId: string, timest
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateStageStatus(event: MaisakaMonitorEvent) {
|
function updateStageStatus(event: MaisakaMonitorEvent) {
|
||||||
|
const applyStatusIfFresh = (next: Map<string, StageStatusInfo>, status: StageStatusInfo) => {
|
||||||
|
const existing = next.get(status.sessionId)
|
||||||
|
if (existing && status.updatedAt < existing.updatedAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.set(status.sessionId, status)
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'stage.snapshot') {
|
if (event.type === 'stage.snapshot') {
|
||||||
const rawEntries = (event.data as unknown as Record<string, unknown>).entries
|
const rawEntries = (event.data as unknown as Record<string, unknown>).entries
|
||||||
if (!Array.isArray(rawEntries)) {
|
if (!Array.isArray(rawEntries)) {
|
||||||
@@ -401,7 +409,7 @@ function updateStageStatus(event: MaisakaMonitorEvent) {
|
|||||||
}
|
}
|
||||||
const status = toStageStatusInfo(rawEntry as Record<string, unknown>)
|
const status = toStageStatusInfo(rawEntry as Record<string, unknown>)
|
||||||
if (status) {
|
if (status) {
|
||||||
next.set(status.sessionId, status)
|
applyStatusIfFresh(next, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedStageStatuses = next
|
cachedStageStatuses = next
|
||||||
@@ -414,7 +422,7 @@ function updateStageStatus(event: MaisakaMonitorEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const next = new Map(cachedStageStatuses)
|
const next = new Map(cachedStageStatuses)
|
||||||
next.set(status.sessionId, status)
|
applyStatusIfFresh(next, status)
|
||||||
cachedStageStatuses = next
|
cachedStageStatuses = next
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
cleanupLocalCache,
|
cleanupLocalCache,
|
||||||
getLocalCacheStats,
|
getLocalCacheStats,
|
||||||
type CacheDirectoryStats,
|
type CacheDirectoryStats,
|
||||||
|
type LocalCacheCleanupTarget,
|
||||||
type LocalCacheStats,
|
type LocalCacheStats,
|
||||||
type LogCleanupTable,
|
type LogCleanupTable,
|
||||||
} from '@/lib/system-api'
|
} from '@/lib/system-api'
|
||||||
@@ -60,9 +61,12 @@ function DirectoryCard({
|
|||||||
}: {
|
}: {
|
||||||
item: CacheDirectoryStats
|
item: CacheDirectoryStats
|
||||||
cleanupDisabled: boolean
|
cleanupDisabled: boolean
|
||||||
onCleanup: (target: 'images' | 'emoji') => void
|
onCleanup: (target: 'images' | 'emoji' | 'log_files') => void
|
||||||
}) {
|
}) {
|
||||||
const cleanupTarget = item.key === 'images' ? 'images' : item.key === 'emoji' ? 'emoji' : null
|
const cleanupTarget = item.key === 'images' ? 'images' : item.key === 'emoji' ? 'emoji' : item.key === 'logs' ? 'log_files' : null
|
||||||
|
const cleanupDescription = cleanupTarget === 'log_files'
|
||||||
|
? '这会删除 logs 目录中的日志文件。操作不可撤销。'
|
||||||
|
: '这会删除对应目录中的文件,并移除数据库里的相关记录。操作不可撤销。'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-card p-4">
|
<div className="rounded-lg border bg-card p-4">
|
||||||
@@ -86,7 +90,7 @@ function DirectoryCard({
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>确认清理{item.label}?</AlertDialogTitle>
|
<AlertDialogTitle>确认清理{item.label}?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
这会删除对应目录中的文件,并移除数据库里的相关记录。操作不可撤销。
|
{cleanupDescription}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -124,7 +128,7 @@ export function LocalCacheTab() {
|
|||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const [stats, setStats] = useState<LocalCacheStats | null>(null)
|
const [stats, setStats] = useState<LocalCacheStats | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [cleanupTarget, setCleanupTarget] = useState<string | null>(null)
|
const [cleanupTarget, setCleanupTarget] = useState<LocalCacheCleanupTarget | null>(null)
|
||||||
const [selectedLogTables, setSelectedLogTables] = useState<LogCleanupTable[]>([])
|
const [selectedLogTables, setSelectedLogTables] = useState<LogCleanupTable[]>([])
|
||||||
|
|
||||||
const tableRows = useMemo(() => {
|
const tableRows = useMemo(() => {
|
||||||
@@ -152,7 +156,7 @@ export function LocalCacheTab() {
|
|||||||
}
|
}
|
||||||
}, [toast])
|
}, [toast])
|
||||||
|
|
||||||
const handleDirectoryCleanup = async (target: 'images' | 'emoji') => {
|
const handleDirectoryCleanup = async (target: 'images' | 'emoji' | 'log_files') => {
|
||||||
setCleanupTarget(target)
|
setCleanupTarget(target)
|
||||||
try {
|
try {
|
||||||
const result = await cleanupLocalCache(target)
|
const result = await cleanupLocalCache(target)
|
||||||
@@ -173,18 +177,18 @@ export function LocalCacheTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogCleanup = async () => {
|
const handleLogCleanup = async () => {
|
||||||
setCleanupTarget('logs')
|
setCleanupTarget('database_logs')
|
||||||
try {
|
try {
|
||||||
const result = await cleanupLocalCache('logs', selectedLogTables)
|
const result = await cleanupLocalCache('database_logs', selectedLogTables)
|
||||||
setSelectedLogTables([])
|
setSelectedLogTables([])
|
||||||
await refreshStats()
|
await refreshStats()
|
||||||
toast({
|
toast({
|
||||||
title: result.message,
|
title: result.message,
|
||||||
description: `已清理 ${result.removed_records} 条日志记录。`,
|
description: `已清理 ${result.removed_records} 条数据库记录。`,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: '日志清理失败',
|
title: '数据库清理失败',
|
||||||
description: error instanceof Error ? error.message : '请稍后重试',
|
description: error instanceof Error ? error.message : '请稍后重试',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
@@ -242,22 +246,22 @@ export function LocalCacheTab() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="flex items-center gap-2 text-base font-semibold sm:text-lg">
|
<h3 className="flex items-center gap-2 text-base font-semibold sm:text-lg">
|
||||||
<Database className="h-5 w-5" />
|
<Database className="h-5 w-5" />
|
||||||
日志清理
|
数据库清理
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-xs text-muted-foreground sm:text-sm">
|
<p className="mt-1 text-xs text-muted-foreground sm:text-sm">
|
||||||
清理运行日志类数据,不会删除图片、表情文件和配置文件。
|
清理数据库中的统计、工具和消息记录,不会删除日志文件、图片、表情文件和配置文件。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="outline" className="gap-2" disabled={cleanupTarget !== null || isLoading}>
|
<Button variant="outline" className="gap-2" disabled={cleanupTarget !== null || isLoading}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
日志清理
|
数据库清理
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>选择要清理的日志范围</AlertDialogTitle>
|
<AlertDialogTitle>选择要清理的数据库记录范围</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
数据库当前占用 {formatBytes(stats?.database.total_size ?? 0)}。请手动勾选需要清理的表,默认不会选择任何内容。
|
数据库当前占用 {formatBytes(stats?.database.total_size ?? 0)}。请手动勾选需要清理的表,默认不会选择任何内容。
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|||||||
@@ -493,7 +493,10 @@ class TestSDK:
|
|||||||
"timeout_ms": timeout_ms,
|
"timeout_ms": timeout_ms,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return SimpleNamespace(error=None, payload={"result": {"ok": True}})
|
return SimpleNamespace(
|
||||||
|
error=None,
|
||||||
|
payload={"success": True, "result": {"success": True, "result": {"ok": True}}},
|
||||||
|
)
|
||||||
|
|
||||||
class DummyPlugin:
|
class DummyPlugin:
|
||||||
def _set_context(self, ctx):
|
def _set_context(self, ctx):
|
||||||
@@ -508,9 +511,36 @@ class TestSDK:
|
|||||||
plugin.ctx._plugin_id = "forged_plugin"
|
plugin.ctx._plugin_id = "forged_plugin"
|
||||||
result = await plugin.ctx.call_capability("send.text", text="hello", stream_id="stream-1")
|
result = await plugin.ctx.call_capability("send.text", text="hello", stream_id="stream-1")
|
||||||
|
|
||||||
assert result == {"ok": True}
|
assert result is True
|
||||||
assert runner._rpc_client.calls[0]["plugin_id"] == "owner_plugin"
|
assert runner._rpc_client.calls[0]["plugin_id"] == "owner_plugin"
|
||||||
assert runner._rpc_client.calls[0]["method"] == "cap.request"
|
assert runner._rpc_client.calls[0]["method"] == "cap.call"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_runner_injected_context_unwraps_llm_available_models(self):
|
||||||
|
"""Runner 应为 SDK 解开 cap.call 响应外层,避免模型列表被规整成空列表。"""
|
||||||
|
from src.plugin_runtime.runner.runner_main import PluginRunner
|
||||||
|
|
||||||
|
class DummyRPCClient:
|
||||||
|
async def send_request(self, method, plugin_id="", payload=None, timeout_ms=30000):
|
||||||
|
assert method == "cap.call"
|
||||||
|
assert plugin_id == "owner_plugin"
|
||||||
|
assert payload == {"capability": "llm.get_available_models", "args": {}}
|
||||||
|
return SimpleNamespace(
|
||||||
|
error=None,
|
||||||
|
payload={"success": True, "result": {"success": True, "models": ["utils", "replyer"]}},
|
||||||
|
)
|
||||||
|
|
||||||
|
class DummyPlugin:
|
||||||
|
def _set_context(self, ctx):
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
runner = PluginRunner(host_address="dummy", session_token="token", plugin_dirs=[])
|
||||||
|
runner._rpc_client = DummyRPCClient()
|
||||||
|
|
||||||
|
plugin = DummyPlugin()
|
||||||
|
runner._inject_context("owner_plugin", plugin)
|
||||||
|
|
||||||
|
assert await plugin.ctx.llm.get_available_models() == ["utils", "replyer"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_runner_applies_initial_plugin_config(self, tmp_path):
|
async def test_runner_applies_initial_plugin_config(self, tmp_path):
|
||||||
@@ -671,7 +701,7 @@ class TestSDK:
|
|||||||
if method == "cap.call":
|
if method == "cap.call":
|
||||||
bootstrap_methods = [call["method"] for call in self.calls[:-1]]
|
bootstrap_methods = [call["method"] for call in self.calls[:-1]]
|
||||||
assert "plugin.bootstrap" in bootstrap_methods
|
assert "plugin.bootstrap" in bootstrap_methods
|
||||||
return SimpleNamespace(error=None, payload={"success": True})
|
return SimpleNamespace(error=None, payload={"success": True, "result": {"success": True}})
|
||||||
return SimpleNamespace(error=None, payload={"accepted": True})
|
return SimpleNamespace(error=None, payload={"accepted": True})
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
@@ -702,11 +732,15 @@ class TestSDK:
|
|||||||
instance=plugin,
|
instance=plugin,
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
capabilities_required=["send.text"],
|
capabilities_required=["send.text"],
|
||||||
|
dependencies=[],
|
||||||
|
manifest=SimpleNamespace(plugin_dependencies=[], llm_provider_client_types=[]),
|
||||||
|
component_handlers={},
|
||||||
|
llm_provider_handlers={},
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(runner, "_install_log_handler", lambda: None)
|
monkeypatch.setattr(runner, "_install_log_handler", lambda: None)
|
||||||
monkeypatch.setattr(runner, "_uninstall_log_handler", lambda: asyncio.sleep(0))
|
monkeypatch.setattr(runner, "_uninstall_log_handler", lambda: asyncio.sleep(0))
|
||||||
monkeypatch.setattr(runner._loader, "discover_and_load", lambda plugin_dirs: [meta])
|
monkeypatch.setattr(runner._loader, "discover_and_load", lambda plugin_dirs, **kwargs: [meta])
|
||||||
|
|
||||||
await runner.run()
|
await runner.run()
|
||||||
|
|
||||||
|
|||||||
@@ -470,6 +470,8 @@ async def emit_cycle_end(
|
|||||||
cycle_id: int,
|
cycle_id: int,
|
||||||
time_records: Dict[str, float],
|
time_records: Dict[str, float],
|
||||||
agent_state: str,
|
agent_state: str,
|
||||||
|
end_reason: str,
|
||||||
|
end_detail: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""广播单个推理循环结束事件。"""
|
"""广播单个推理循环结束事件。"""
|
||||||
|
|
||||||
@@ -478,6 +480,8 @@ async def emit_cycle_end(
|
|||||||
"cycle_id": cycle_id,
|
"cycle_id": cycle_id,
|
||||||
"time_records": _normalize_payload_value(time_records),
|
"time_records": _normalize_payload_value(time_records),
|
||||||
"agent_state": agent_state,
|
"agent_state": agent_state,
|
||||||
|
"end_reason": end_reason,
|
||||||
|
"end_detail": end_detail,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -533,10 +537,13 @@ async def emit_planner_finalized(
|
|||||||
planner_completion_tokens: Optional[int],
|
planner_completion_tokens: Optional[int],
|
||||||
planner_total_tokens: Optional[int],
|
planner_total_tokens: Optional[int],
|
||||||
planner_duration_ms: Optional[float],
|
planner_duration_ms: Optional[float],
|
||||||
planner_prompt_html_uri: Optional[str],
|
planner_prompt_html_uri: Optional[str] = None,
|
||||||
tools: Optional[List[Dict[str, Any]]],
|
tools: Optional[List[Dict[str, Any]]] = None,
|
||||||
time_records: Dict[str, float],
|
time_records: Optional[Dict[str, float]] = None,
|
||||||
agent_state: str,
|
agent_state: str = "",
|
||||||
|
planner_interrupted: bool = False,
|
||||||
|
end_reason: str = "",
|
||||||
|
end_detail: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""广播一轮 planner 结束后的最终聚合事件。"""
|
"""广播一轮 planner 结束后的最终聚合事件。"""
|
||||||
|
|
||||||
@@ -572,8 +579,11 @@ async def emit_planner_finalized(
|
|||||||
planner_prompt_html_uri,
|
planner_prompt_html_uri,
|
||||||
),
|
),
|
||||||
"tools": _serialize_tool_results(list(tools or [])),
|
"tools": _serialize_tool_results(list(tools or [])),
|
||||||
|
"interrupted": planner_interrupted,
|
||||||
"final_state": {
|
"final_state": {
|
||||||
"time_records": _normalize_payload_value(time_records),
|
"time_records": _normalize_payload_value(time_records or {}),
|
||||||
"agent_state": agent_state,
|
"agent_state": agent_state,
|
||||||
|
"end_reason": end_reason,
|
||||||
|
"end_detail": end_detail,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ class MaisakaReasoningEngine:
|
|||||||
max_internal_rounds: int,
|
max_internal_rounds: int,
|
||||||
has_pending_messages: bool,
|
has_pending_messages: bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return has_pending_messages and round_index + 1 < max_internal_rounds
|
return has_pending_messages and round_index < max_internal_rounds
|
||||||
|
|
||||||
async def run_loop(self) -> None:
|
async def run_loop(self) -> None:
|
||||||
"""独立消费消息批次,并执行对应的内部思考轮次。"""
|
"""独立消费消息批次,并执行对应的内部思考轮次。"""
|
||||||
@@ -443,7 +443,8 @@ class MaisakaReasoningEngine:
|
|||||||
anchor_message = cached_messages[-1]
|
anchor_message = cached_messages[-1]
|
||||||
try:
|
try:
|
||||||
timing_gate_required = True
|
timing_gate_required = True
|
||||||
for round_index in range(self._runtime._max_internal_rounds):
|
round_index = 0
|
||||||
|
while round_index < self._runtime._max_internal_rounds:
|
||||||
cycle_detail = self._start_cycle()
|
cycle_detail = self._start_cycle()
|
||||||
round_text = f"第 {round_index + 1}/{self._runtime._max_internal_rounds} 轮"
|
round_text = f"第 {round_index + 1}/{self._runtime._max_internal_rounds} 轮"
|
||||||
self._runtime._log_cycle_started(cycle_detail, round_index)
|
self._runtime._log_cycle_started(cycle_detail, round_index)
|
||||||
@@ -466,6 +467,9 @@ class MaisakaReasoningEngine:
|
|||||||
response: Optional[ChatResponse] = None
|
response: Optional[ChatResponse] = None
|
||||||
action_tool_definitions: list[dict[str, Any]] = []
|
action_tool_definitions: list[dict[str, Any]] = []
|
||||||
planner_extra_lines: list[str] = []
|
planner_extra_lines: list[str] = []
|
||||||
|
planner_interrupted = False
|
||||||
|
cycle_end_reason = "continue"
|
||||||
|
cycle_end_detail = "本轮思考完成,继续后续内部轮次。"
|
||||||
tool_result_summaries: list[str] = []
|
tool_result_summaries: list[str] = []
|
||||||
tool_monitor_results: list[dict[str, Any]] = []
|
tool_monitor_results: list[dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
@@ -507,6 +511,8 @@ class MaisakaReasoningEngine:
|
|||||||
)
|
)
|
||||||
timing_gate_required = self._mark_timing_gate_completed(timing_action)
|
timing_gate_required = self._mark_timing_gate_completed(timing_action)
|
||||||
if timing_action != "continue":
|
if timing_action != "continue":
|
||||||
|
cycle_end_reason = "timing_no_reply"
|
||||||
|
cycle_end_detail = "Timing Gate 选择 no_reply,本轮不会进入 Planner。"
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
|
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
|
||||||
f"回合={round_index + 1} 动作={timing_action}"
|
f"回合={round_index + 1} 动作={timing_action}"
|
||||||
@@ -551,19 +557,40 @@ class MaisakaReasoningEngine:
|
|||||||
|
|
||||||
if response.tool_calls:
|
if response.tool_calls:
|
||||||
tool_started_at = time.time()
|
tool_started_at = time.time()
|
||||||
should_pause, tool_result_summaries, tool_monitor_results = await self._handle_tool_calls(
|
(
|
||||||
|
should_pause,
|
||||||
|
pause_tool_name,
|
||||||
|
tool_result_summaries,
|
||||||
|
tool_monitor_results,
|
||||||
|
) = await self._handle_tool_calls(
|
||||||
response.tool_calls,
|
response.tool_calls,
|
||||||
response.content or "",
|
response.content or "",
|
||||||
anchor_message,
|
anchor_message,
|
||||||
)
|
)
|
||||||
cycle_detail.time_records["tool_calls"] = time.time() - tool_started_at
|
cycle_detail.time_records["tool_calls"] = time.time() - tool_started_at
|
||||||
if should_pause:
|
if should_pause:
|
||||||
|
if pause_tool_name == "finish":
|
||||||
|
cycle_end_reason = "finish"
|
||||||
|
cycle_end_detail = "Planner 调用 finish,结束本轮思考并等待新消息。"
|
||||||
|
elif pause_tool_name:
|
||||||
|
cycle_end_reason = f"tool_pause:{pause_tool_name}"
|
||||||
|
cycle_end_detail = f"工具 {pause_tool_name} 要求暂停当前思考循环。"
|
||||||
|
else:
|
||||||
|
cycle_end_reason = "tool_pause"
|
||||||
|
cycle_end_detail = "工具要求暂停当前思考循环。"
|
||||||
break
|
break
|
||||||
|
cycle_end_reason = "tool_continue"
|
||||||
|
cycle_end_detail = "Planner 工具执行完成,继续下一轮内部思考。"
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not response.content:
|
if not response.content:
|
||||||
|
cycle_end_reason = "empty_planner_response"
|
||||||
|
cycle_end_detail = "Planner 没有返回文本或工具调用,本轮思考结束。"
|
||||||
break
|
break
|
||||||
except ReqAbortException as exc:
|
except ReqAbortException as exc:
|
||||||
|
planner_interrupted = True
|
||||||
|
cycle_end_reason = "planner_interrupted"
|
||||||
|
cycle_end_detail = "Planner 被新消息打断,当前轮结束。"
|
||||||
self._runtime._update_stage_status(
|
self._runtime._update_stage_status(
|
||||||
"Planner 已打断",
|
"Planner 已打断",
|
||||||
str(exc) or "收到外部中断信号",
|
str(exc) or "收到外部中断信号",
|
||||||
@@ -627,6 +654,15 @@ class MaisakaReasoningEngine:
|
|||||||
continue
|
continue
|
||||||
finally:
|
finally:
|
||||||
completed_cycle = self._end_cycle(cycle_detail)
|
completed_cycle = self._end_cycle(cycle_detail)
|
||||||
|
if (
|
||||||
|
round_index + 1 >= self._runtime._max_internal_rounds
|
||||||
|
and cycle_end_reason in {"continue", "tool_continue"}
|
||||||
|
):
|
||||||
|
cycle_end_reason = "max_rounds"
|
||||||
|
cycle_end_detail = (
|
||||||
|
f"已达到内部思考轮次上限 {self._runtime._max_internal_rounds},"
|
||||||
|
"本轮处理结束。"
|
||||||
|
)
|
||||||
self._runtime._render_context_usage_panel(
|
self._runtime._render_context_usage_panel(
|
||||||
cycle_id=cycle_detail.cycle_id,
|
cycle_id=cycle_detail.cycle_id,
|
||||||
time_records=dict(completed_cycle.time_records),
|
time_records=dict(completed_cycle.time_records),
|
||||||
@@ -692,13 +728,20 @@ class MaisakaReasoningEngine:
|
|||||||
tools=tool_monitor_results,
|
tools=tool_monitor_results,
|
||||||
time_records=dict(completed_cycle.time_records),
|
time_records=dict(completed_cycle.time_records),
|
||||||
agent_state=self._runtime._agent_state,
|
agent_state=self._runtime._agent_state,
|
||||||
|
planner_interrupted=planner_interrupted,
|
||||||
|
end_reason=cycle_end_reason,
|
||||||
|
end_detail=cycle_end_detail,
|
||||||
)
|
)
|
||||||
await emit_cycle_end(
|
await emit_cycle_end(
|
||||||
session_id=self._runtime.session_id,
|
session_id=self._runtime.session_id,
|
||||||
cycle_id=cycle_detail.cycle_id,
|
cycle_id=cycle_detail.cycle_id,
|
||||||
time_records=dict(completed_cycle.time_records),
|
time_records=dict(completed_cycle.time_records),
|
||||||
agent_state=self._runtime._agent_state,
|
agent_state=self._runtime._agent_state,
|
||||||
|
end_reason=cycle_end_reason,
|
||||||
|
end_detail=cycle_end_detail,
|
||||||
)
|
)
|
||||||
|
if not planner_interrupted:
|
||||||
|
round_index += 1
|
||||||
finally:
|
finally:
|
||||||
if self._runtime._agent_state == self._runtime._STATE_RUNNING:
|
if self._runtime._agent_state == self._runtime._STATE_RUNNING:
|
||||||
self._runtime._agent_state = self._runtime._STATE_STOP
|
self._runtime._agent_state = self._runtime._STATE_STOP
|
||||||
@@ -1371,7 +1414,7 @@ class MaisakaReasoningEngine:
|
|||||||
tool_calls: list[ToolCall],
|
tool_calls: list[ToolCall],
|
||||||
latest_thought: str,
|
latest_thought: str,
|
||||||
anchor_message: SessionMessage,
|
anchor_message: SessionMessage,
|
||||||
) -> tuple[bool, list[str], list[dict[str, Any]]]:
|
) -> tuple[bool, str, list[str], list[dict[str, Any]]]:
|
||||||
"""执行一批统一工具调用。
|
"""执行一批统一工具调用。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1380,8 +1423,8 @@ class MaisakaReasoningEngine:
|
|||||||
anchor_message: 当前轮的锚点消息。
|
anchor_message: 当前轮的锚点消息。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[bool, list[str], list[dict[str, Any]]]: 是否需要暂停当前思考循环、
|
tuple[bool, str, list[str], list[dict[str, Any]]]: 是否需要暂停当前思考循环、
|
||||||
工具结果摘要列表,以及最终监控事件使用的工具详情列表。
|
触发暂停的工具名、工具结果摘要列表,以及最终监控事件使用的工具详情列表。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tool_result_summaries: list[str] = []
|
tool_result_summaries: list[str] = []
|
||||||
@@ -1401,7 +1444,7 @@ class MaisakaReasoningEngine:
|
|||||||
tool_monitor_results.append(
|
tool_monitor_results.append(
|
||||||
self._build_tool_monitor_result(tool_call, invocation, result, duration_ms=0.0, tool_spec=None)
|
self._build_tool_monitor_result(tool_call, invocation, result, duration_ms=0.0, tool_spec=None)
|
||||||
)
|
)
|
||||||
return False, tool_result_summaries, tool_monitor_results
|
return False, "", tool_result_summaries, tool_monitor_results
|
||||||
|
|
||||||
execution_context = self._build_tool_execution_context(latest_thought, anchor_message)
|
execution_context = self._build_tool_execution_context(latest_thought, anchor_message)
|
||||||
availability_context = self._build_tool_availability_context()
|
availability_context = self._build_tool_availability_context()
|
||||||
@@ -1450,6 +1493,6 @@ class MaisakaReasoningEngine:
|
|||||||
logger.warning(f"{self._runtime.log_prefix} 回复工具未生成可见消息,将继续下一轮循环")
|
logger.warning(f"{self._runtime.log_prefix} 回复工具未生成可见消息,将继续下一轮循环")
|
||||||
|
|
||||||
if bool(result.metadata.get("pause_execution", False)):
|
if bool(result.metadata.get("pause_execution", False)):
|
||||||
return True, tool_result_summaries, tool_monitor_results
|
return True, invocation.tool_name, tool_result_summaries, tool_monitor_results
|
||||||
|
|
||||||
return False, tool_result_summaries, tool_monitor_results
|
return False, "", tool_result_summaries, tool_monitor_results
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from .tool_provider import MaisakaBuiltinToolProvider
|
|||||||
|
|
||||||
logger = get_logger("maisaka_runtime")
|
logger = get_logger("maisaka_runtime")
|
||||||
|
|
||||||
MAX_INTERNAL_ROUNDS = 6
|
MAX_INTERNAL_ROUNDS = 10
|
||||||
|
|
||||||
|
|
||||||
class MaisakaHeartFlowChatting:
|
class MaisakaHeartFlowChatting:
|
||||||
|
|||||||
@@ -517,6 +517,8 @@ class PluginRunner:
|
|||||||
)
|
)
|
||||||
if resp.error:
|
if resp.error:
|
||||||
raise RuntimeError(resp.error.get("message", "能力调用失败"))
|
raise RuntimeError(resp.error.get("message", "能力调用失败"))
|
||||||
|
if normalized_method == "cap.call" and isinstance(resp.payload, dict) and "result" in resp.payload:
|
||||||
|
return resp.payload.get("result")
|
||||||
return resp.payload
|
return resp.payload
|
||||||
|
|
||||||
ctx = PluginContext(plugin_id=plugin_id, rpc_call=_rpc_call)
|
ctx = PluginContext(plugin_id=plugin_id, rpc_call=_rpc_call)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class LocalCacheStatsResponse(BaseModel):
|
|||||||
class LocalCacheCleanupRequest(BaseModel):
|
class LocalCacheCleanupRequest(BaseModel):
|
||||||
"""本地缓存清理请求。"""
|
"""本地缓存清理请求。"""
|
||||||
|
|
||||||
target: Literal["images", "emoji", "logs"]
|
target: Literal["images", "emoji", "log_files", "database_logs"]
|
||||||
tables: list[Literal["llm_usage", "tool_records", "mai_messages"]] = Field(default_factory=list)
|
tables: list[Literal["llm_usage", "tool_records", "mai_messages"]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@@ -385,13 +385,23 @@ async def cleanup_local_cache(request: LocalCacheCleanupRequest):
|
|||||||
removed_records=removed_records,
|
removed_records=removed_records,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if request.target == "log_files":
|
||||||
|
removed_files, removed_bytes = _remove_directory_contents(_LOG_DIR)
|
||||||
|
return LocalCacheCleanupResponse(
|
||||||
|
success=True,
|
||||||
|
message="日志文件已清理",
|
||||||
|
target=request.target,
|
||||||
|
removed_files=removed_files,
|
||||||
|
removed_bytes=removed_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
if not request.tables:
|
if not request.tables:
|
||||||
raise HTTPException(status_code=400, detail="请至少选择一个要清理的日志表")
|
raise HTTPException(status_code=400, detail="请至少选择一个要清理的数据库表")
|
||||||
|
|
||||||
removed_records = _delete_log_records(list(request.tables))
|
removed_records = _delete_log_records(list(request.tables))
|
||||||
return LocalCacheCleanupResponse(
|
return LocalCacheCleanupResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message="日志记录已清理",
|
message="数据库日志记录已清理",
|
||||||
target=request.target,
|
target=request.target,
|
||||||
removed_records=removed_records,
|
removed_records=removed_records,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user