diff --git a/dashboard/src/lib/maisaka-monitor-client.ts b/dashboard/src/lib/maisaka-monitor-client.ts index dedbe568..4a6458af 100644 --- a/dashboard/src/lib/maisaka-monitor-client.ts +++ b/dashboard/src/lib/maisaka-monitor-client.ts @@ -175,9 +175,12 @@ export interface PlannerFinalizedEvent { request: MaisakaRequestBlock | null planner: MaisakaPlannerBlock | null tools: MaisakaFinalizedToolResult[] + interrupted?: boolean final_state: { time_records: Record agent_state: string + end_reason?: string + end_detail?: string } } @@ -186,6 +189,8 @@ export interface CycleEndEvent { cycle_id: number time_records: Record agent_state: string + end_reason?: string + end_detail?: string timestamp: number } diff --git a/dashboard/src/lib/system-api.ts b/dashboard/src/lib/system-api.ts index 7a3c333e..9b880c6c 100644 --- a/dashboard/src/lib/system-api.ts +++ b/dashboard/src/lib/system-api.ts @@ -86,7 +86,7 @@ export interface LocalCacheStats { export interface LocalCacheCleanupResult { success: boolean message: string - target: 'images' | 'emoji' | 'logs' + target: 'images' | 'emoji' | 'log_files' | 'database_logs' removed_files: number removed_bytes: number removed_records: number diff --git a/dashboard/src/routes/monitor/maisaka-monitor.tsx b/dashboard/src/routes/monitor/maisaka-monitor.tsx index 95e286b6..77aefb86 100644 --- a/dashboard/src/routes/monitor/maisaka-monitor.tsx +++ b/dashboard/src/routes/monitor/maisaka-monitor.tsx @@ -6,6 +6,7 @@ */ import { Activity, + AlertCircle, ArrowRight, Bot, Brain, @@ -315,6 +316,38 @@ function openPromptHtml(uri: string) { 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 ( +
+
+ + Planner 被新消息打断 + + #{data.cycle_id} + + {planner && planner.duration_ms > 0 && ( + {formatMs(planner.duration_ms)} + )} +
+

+ {planner?.content || '收到新消息,已停止当前思考并准备重新决策。'} +

+
+ ) +} + function PlannerResponseCard({ data }: { data: PlannerResponseEvent }) { return (
@@ -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 }) { const totalTime = Object.values(data.time_records).reduce((a, b) => a + b, 0) return ( -
- -
- - 循环结束 - - #{data.cycle_id} - - {formatMs(totalTime * 1000)} - - {data.agent_state} - +
+
+ +
+ + {getCycleEndReasonLabel(data)} + + #{data.cycle_id} + + {formatMs(totalTime * 1000)} + + {data.agent_state} + +
+
- +

{getCycleEndReasonText(data)}

) } @@ -648,7 +717,10 @@ function TimelineEventRenderer({ case 'planner.response': return case 'planner.finalized': - if ((entry.data as PlannerFinalizedEvent).timing_gate?.result?.action !== 'continue') { + if (isPlannerInterrupted(entry.data as PlannerFinalizedEvent)) { + return + } + if ((entry.data as PlannerFinalizedEvent).timing_gate?.result?.action === 'no_reply') { return null } return ( @@ -856,23 +928,32 @@ export function MaisakaMonitor() {
) : ( (() => { - const continuedTimingGateCycles = new Set() - const stoppedTimingGateCycles = new Set() + const noReplyTimingGateCycles = new Set() return timeline.map((entry) => { if (entry.type === 'timing_gate.result') { const data = entry.data as TimingGateResultEvent - if (data.action === 'continue') { - continuedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id)) - } else { - stoppedTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id)) + if (data.action === 'no_reply') { + noReplyTimingGateCycles.add(buildCycleKey(data.session_id, data.cycle_id)) } } if (entry.type === 'planner.response' || entry.type === 'planner.finalized') { const data = entry.data as PlannerResponseEvent | PlannerFinalizedEvent 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 = + if (!rendered) return null + return ( +
+ {rendered} +
+ ) + } + if (noReplyTimingGateCycles.has(cycleKey)) { return null } } @@ -897,3 +978,4 @@ export function MaisakaMonitor() {
) } + diff --git a/dashboard/src/routes/monitor/use-maisaka-monitor.ts b/dashboard/src/routes/monitor/use-maisaka-monitor.ts index 6ca75350..93869139 100644 --- a/dashboard/src/routes/monitor/use-maisaka-monitor.ts +++ b/dashboard/src/routes/monitor/use-maisaka-monitor.ts @@ -389,6 +389,14 @@ function updateSessionInfo(event: MaisakaMonitorEvent, sessionId: string, timest } function updateStageStatus(event: MaisakaMonitorEvent) { + const applyStatusIfFresh = (next: Map, 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') { const rawEntries = (event.data as unknown as Record).entries if (!Array.isArray(rawEntries)) { @@ -401,7 +409,7 @@ function updateStageStatus(event: MaisakaMonitorEvent) { } const status = toStageStatusInfo(rawEntry as Record) if (status) { - next.set(status.sessionId, status) + applyStatusIfFresh(next, status) } } cachedStageStatuses = next @@ -414,7 +422,7 @@ function updateStageStatus(event: MaisakaMonitorEvent) { return } const next = new Map(cachedStageStatuses) - next.set(status.sessionId, status) + applyStatusIfFresh(next, status) cachedStageStatuses = next return } diff --git a/dashboard/src/routes/settings/LocalCacheTab.tsx b/dashboard/src/routes/settings/LocalCacheTab.tsx index b19350a8..20384f9c 100644 --- a/dashboard/src/routes/settings/LocalCacheTab.tsx +++ b/dashboard/src/routes/settings/LocalCacheTab.tsx @@ -19,6 +19,7 @@ import { cleanupLocalCache, getLocalCacheStats, type CacheDirectoryStats, + type LocalCacheCleanupTarget, type LocalCacheStats, type LogCleanupTable, } from '@/lib/system-api' @@ -60,9 +61,12 @@ function DirectoryCard({ }: { item: CacheDirectoryStats 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 (
@@ -86,7 +90,7 @@ function DirectoryCard({ 确认清理{item.label}? - 这会删除对应目录中的文件,并移除数据库里的相关记录。操作不可撤销。 + {cleanupDescription} @@ -124,7 +128,7 @@ export function LocalCacheTab() { const { toast } = useToast() const [stats, setStats] = useState(null) const [isLoading, setIsLoading] = useState(false) - const [cleanupTarget, setCleanupTarget] = useState(null) + const [cleanupTarget, setCleanupTarget] = useState(null) const [selectedLogTables, setSelectedLogTables] = useState([]) const tableRows = useMemo(() => { @@ -152,7 +156,7 @@ export function LocalCacheTab() { } }, [toast]) - const handleDirectoryCleanup = async (target: 'images' | 'emoji') => { + const handleDirectoryCleanup = async (target: 'images' | 'emoji' | 'log_files') => { setCleanupTarget(target) try { const result = await cleanupLocalCache(target) @@ -173,18 +177,18 @@ export function LocalCacheTab() { } const handleLogCleanup = async () => { - setCleanupTarget('logs') + setCleanupTarget('database_logs') try { - const result = await cleanupLocalCache('logs', selectedLogTables) + const result = await cleanupLocalCache('database_logs', selectedLogTables) setSelectedLogTables([]) await refreshStats() toast({ title: result.message, - description: `已清理 ${result.removed_records} 条日志记录。`, + description: `已清理 ${result.removed_records} 条数据库记录。`, }) } catch (error) { toast({ - title: '日志清理失败', + title: '数据库清理失败', description: error instanceof Error ? error.message : '请稍后重试', variant: 'destructive', }) @@ -242,22 +246,22 @@ export function LocalCacheTab() {

- 日志清理 + 数据库清理

- 清理运行日志类数据,不会删除图片、表情文件和配置文件。 + 清理数据库中的统计、工具和消息记录,不会删除日志文件、图片、表情文件和配置文件。

- 选择要清理的日志范围 + 选择要清理的数据库记录范围 数据库当前占用 {formatBytes(stats?.database.total_size ?? 0)}。请手动勾选需要清理的表,默认不会选择任何内容。 diff --git a/pytests/test_plugin_runtime.py b/pytests/test_plugin_runtime.py index 840f2dcf..c8daff96 100644 --- a/pytests/test_plugin_runtime.py +++ b/pytests/test_plugin_runtime.py @@ -493,7 +493,10 @@ class TestSDK: "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: def _set_context(self, ctx): @@ -508,9 +511,36 @@ class TestSDK: plugin.ctx._plugin_id = "forged_plugin" 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]["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 async def test_runner_applies_initial_plugin_config(self, tmp_path): @@ -671,7 +701,7 @@ class TestSDK: if method == "cap.call": bootstrap_methods = [call["method"] for call in self.calls[:-1]] 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}) async def disconnect(self): @@ -702,11 +732,15 @@ class TestSDK: instance=plugin, version="1.0.0", 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, "_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() diff --git a/src/maisaka/monitor_events.py b/src/maisaka/monitor_events.py index 6aa5ce51..3b652251 100644 --- a/src/maisaka/monitor_events.py +++ b/src/maisaka/monitor_events.py @@ -470,6 +470,8 @@ async def emit_cycle_end( cycle_id: int, time_records: Dict[str, float], agent_state: str, + end_reason: str, + end_detail: str, ) -> None: """广播单个推理循环结束事件。""" @@ -478,6 +480,8 @@ async def emit_cycle_end( "cycle_id": cycle_id, "time_records": _normalize_payload_value(time_records), "agent_state": agent_state, + "end_reason": end_reason, + "end_detail": end_detail, "timestamp": time.time(), }) @@ -533,10 +537,13 @@ async def emit_planner_finalized( planner_completion_tokens: Optional[int], planner_total_tokens: Optional[int], planner_duration_ms: Optional[float], - planner_prompt_html_uri: Optional[str], - tools: Optional[List[Dict[str, Any]]], - time_records: Dict[str, float], - agent_state: str, + planner_prompt_html_uri: Optional[str] = None, + tools: Optional[List[Dict[str, Any]]] = None, + time_records: Optional[Dict[str, float]] = None, + agent_state: str = "", + planner_interrupted: bool = False, + end_reason: str = "", + end_detail: str = "", ) -> None: """广播一轮 planner 结束后的最终聚合事件。""" @@ -572,8 +579,11 @@ async def emit_planner_finalized( planner_prompt_html_uri, ), "tools": _serialize_tool_results(list(tools or [])), + "interrupted": planner_interrupted, "final_state": { - "time_records": _normalize_payload_value(time_records), + "time_records": _normalize_payload_value(time_records or {}), "agent_state": agent_state, + "end_reason": end_reason, + "end_detail": end_detail, }, }) diff --git a/src/maisaka/reasoning_engine.py b/src/maisaka/reasoning_engine.py index 2fbb6337..31b838c4 100644 --- a/src/maisaka/reasoning_engine.py +++ b/src/maisaka/reasoning_engine.py @@ -412,7 +412,7 @@ class MaisakaReasoningEngine: max_internal_rounds: int, has_pending_messages: 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: """独立消费消息批次,并执行对应的内部思考轮次。""" @@ -443,7 +443,8 @@ class MaisakaReasoningEngine: anchor_message = cached_messages[-1] try: 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() round_text = f"第 {round_index + 1}/{self._runtime._max_internal_rounds} 轮" self._runtime._log_cycle_started(cycle_detail, round_index) @@ -466,6 +467,9 @@ class MaisakaReasoningEngine: response: Optional[ChatResponse] = None action_tool_definitions: list[dict[str, Any]] = [] planner_extra_lines: list[str] = [] + planner_interrupted = False + cycle_end_reason = "continue" + cycle_end_detail = "本轮思考完成,继续后续内部轮次。" tool_result_summaries: list[str] = [] tool_monitor_results: list[dict[str, Any]] = [] try: @@ -507,6 +511,8 @@ class MaisakaReasoningEngine: ) timing_gate_required = self._mark_timing_gate_completed(timing_action) if timing_action != "continue": + cycle_end_reason = "timing_no_reply" + cycle_end_detail = "Timing Gate 选择 no_reply,本轮不会进入 Planner。" logger.debug( f"{self._runtime.log_prefix} Timing Gate 结束当前回合: " f"回合={round_index + 1} 动作={timing_action}" @@ -551,19 +557,40 @@ class MaisakaReasoningEngine: if response.tool_calls: 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.content or "", anchor_message, ) cycle_detail.time_records["tool_calls"] = time.time() - tool_started_at 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 + cycle_end_reason = "tool_continue" + cycle_end_detail = "Planner 工具执行完成,继续下一轮内部思考。" continue if not response.content: + cycle_end_reason = "empty_planner_response" + cycle_end_detail = "Planner 没有返回文本或工具调用,本轮思考结束。" break except ReqAbortException as exc: + planner_interrupted = True + cycle_end_reason = "planner_interrupted" + cycle_end_detail = "Planner 被新消息打断,当前轮结束。" self._runtime._update_stage_status( "Planner 已打断", str(exc) or "收到外部中断信号", @@ -627,6 +654,15 @@ class MaisakaReasoningEngine: continue finally: 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( cycle_id=cycle_detail.cycle_id, time_records=dict(completed_cycle.time_records), @@ -692,13 +728,20 @@ class MaisakaReasoningEngine: tools=tool_monitor_results, time_records=dict(completed_cycle.time_records), agent_state=self._runtime._agent_state, + planner_interrupted=planner_interrupted, + end_reason=cycle_end_reason, + end_detail=cycle_end_detail, ) await emit_cycle_end( session_id=self._runtime.session_id, cycle_id=cycle_detail.cycle_id, time_records=dict(completed_cycle.time_records), agent_state=self._runtime._agent_state, + end_reason=cycle_end_reason, + end_detail=cycle_end_detail, ) + if not planner_interrupted: + round_index += 1 finally: if self._runtime._agent_state == self._runtime._STATE_RUNNING: self._runtime._agent_state = self._runtime._STATE_STOP @@ -1371,7 +1414,7 @@ class MaisakaReasoningEngine: tool_calls: list[ToolCall], latest_thought: str, anchor_message: SessionMessage, - ) -> tuple[bool, list[str], list[dict[str, Any]]]: + ) -> tuple[bool, str, list[str], list[dict[str, Any]]]: """执行一批统一工具调用。 Args: @@ -1380,8 +1423,8 @@ class MaisakaReasoningEngine: anchor_message: 当前轮的锚点消息。 Returns: - tuple[bool, list[str], list[dict[str, Any]]]: 是否需要暂停当前思考循环、 - 工具结果摘要列表,以及最终监控事件使用的工具详情列表。 + tuple[bool, str, list[str], list[dict[str, Any]]]: 是否需要暂停当前思考循环、 + 触发暂停的工具名、工具结果摘要列表,以及最终监控事件使用的工具详情列表。 """ tool_result_summaries: list[str] = [] @@ -1401,7 +1444,7 @@ class MaisakaReasoningEngine: tool_monitor_results.append( 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) availability_context = self._build_tool_availability_context() @@ -1450,6 +1493,6 @@ class MaisakaReasoningEngine: logger.warning(f"{self._runtime.log_prefix} 回复工具未生成可见消息,将继续下一轮循环") 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 diff --git a/src/maisaka/runtime.py b/src/maisaka/runtime.py index 7e19ecba..ff453910 100644 --- a/src/maisaka/runtime.py +++ b/src/maisaka/runtime.py @@ -55,7 +55,7 @@ from .tool_provider import MaisakaBuiltinToolProvider logger = get_logger("maisaka_runtime") -MAX_INTERNAL_ROUNDS = 6 +MAX_INTERNAL_ROUNDS = 10 class MaisakaHeartFlowChatting: diff --git a/src/plugin_runtime/runner/runner_main.py b/src/plugin_runtime/runner/runner_main.py index a2e5f460..bc6f92be 100644 --- a/src/plugin_runtime/runner/runner_main.py +++ b/src/plugin_runtime/runner/runner_main.py @@ -517,6 +517,8 @@ class PluginRunner: ) if resp.error: 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 ctx = PluginContext(plugin_id=plugin_id, rpc_call=_rpc_call) diff --git a/src/webui/routers/system.py b/src/webui/routers/system.py index 345a6754..5c9af8ed 100644 --- a/src/webui/routers/system.py +++ b/src/webui/routers/system.py @@ -114,7 +114,7 @@ class LocalCacheStatsResponse(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) @@ -385,13 +385,23 @@ async def cleanup_local_cache(request: LocalCacheCleanupRequest): 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: - raise HTTPException(status_code=400, detail="请至少选择一个要清理的日志表") + raise HTTPException(status_code=400, detail="请至少选择一个要清理的数据库表") removed_records = _delete_log_records(list(request.tables)) return LocalCacheCleanupResponse( success=True, - message="日志记录已清理", + message="数据库日志记录已清理", target=request.target, removed_records=removed_records, )