diff --git a/backend/main.py b/backend/main.py index c3e2620..b9958fa 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1558,6 +1558,17 @@ def _clear_bot_sessions(bot_id: str) -> int: return deleted +def _clear_bot_dashboard_direct_session(bot_id: str) -> Dict[str, Any]: + """Truncate the dashboard:direct session file while preserving the workspace session root.""" + root = _sessions_root(bot_id) + os.makedirs(root, exist_ok=True) + path = os.path.join(root, "dashboard_direct.jsonl") + existed = os.path.exists(path) + with open(path, "w", encoding="utf-8"): + pass + return {"path": path, "existed": existed} + + def _read_env_store(bot_id: str) -> Dict[str, str]: path = _env_store_path(bot_id) if not os.path.isfile(path): @@ -3021,6 +3032,34 @@ def clear_bot_messages(bot_id: str, session: Session = Depends(get_session)): return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions} +@app.post("/api/bots/{bot_id}/sessions/dashboard-direct/clear") +def clear_bot_dashboard_direct_session(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + result = _clear_bot_dashboard_direct_session(bot_id) + if str(bot.docker_status or "").upper() == "RUNNING": + try: + docker_manager.send_command(bot_id, "/new") + except Exception: + pass + + bot.updated_at = datetime.utcnow() + session.add(bot) + record_activity_event( + session, + bot_id, + "dashboard_session_cleared", + channel="dashboard", + detail="Cleared dashboard_direct session file", + metadata={"session_file": result["path"], "previously_existed": result["existed"]}, + ) + session.commit() + _invalidate_bot_detail_cache(bot_id) + return {"bot_id": bot_id, "cleared": True, "session_file": result["path"], "previously_existed": result["existed"]} + + @app.get("/api/bots/{bot_id}/logs") def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)): bot = session.get(BotInstance, bot_id) diff --git a/bot-images/litellm_provider.py b/bot-images/litellm_provider.py index e2e0f10..d14e4c0 100644 --- a/bot-images/litellm_provider.py +++ b/bot-images/litellm_provider.py @@ -1,8 +1,6 @@ """LiteLLM provider implementation for multi-provider support.""" -import ast import hashlib -import json import os import secrets import string @@ -20,49 +18,12 @@ from nanobot.providers.registry import find_by_model, find_gateway _ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"}) _ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"}) _ALNUM = string.ascii_letters + string.digits -_ARG_PATCH_VERBOSE_VALUES = {"1", "true", "yes", "on"} def _short_tool_id() -> str: """Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral).""" return "".join(secrets.choice(_ALNUM) for _ in range(9)) -def _should_log_argument_patch() -> bool: - value = str(os.getenv("DASHBOARD_LITELLM_PATCH_VERBOSE") or "").strip().lower() - return value in _ARG_PATCH_VERBOSE_VALUES - - -def _coerce_tool_arguments_json(value: Any) -> str: - """Return provider-safe JSON text for OpenAI-style function.arguments.""" - if value is None: - return "{}" - - if isinstance(value, str): - text = value.strip() - if not text: - return "{}" - try: - json.loads(text) - return text - except Exception: - pass - try: - parsed = ast.literal_eval(text) - except Exception: - parsed = None - else: - try: - return json.dumps(parsed, ensure_ascii=False) - except Exception: - pass - return json.dumps({"raw": text}, ensure_ascii=False) - - try: - return json.dumps(value, ensure_ascii=False) - except Exception: - return json.dumps({"raw": str(value)}, ensure_ascii=False) - - class LiteLLMProvider(LLMProvider): """ LLM provider using LiteLLM for multi-provider support. @@ -222,7 +183,6 @@ class LiteLLMProvider(LLMProvider): allowed = _ALLOWED_MSG_KEYS | extra_keys sanitized = LLMProvider._sanitize_request_messages(messages, allowed) id_map: dict[str, str] = {} - patched_arguments = 0 def map_id(value: Any) -> Any: if not isinstance(value, str): @@ -240,26 +200,11 @@ class LiteLLMProvider(LLMProvider): continue tc_clean = dict(tc) tc_clean["id"] = map_id(tc_clean.get("id")) - function = tc_clean.get("function") - if isinstance(function, dict) and "arguments" in function: - function_clean = dict(function) - original_arguments = function_clean.get("arguments") - normalized_arguments = _coerce_tool_arguments_json(original_arguments) - if original_arguments != normalized_arguments: - patched_arguments += 1 - function_clean["arguments"] = normalized_arguments - tc_clean["function"] = function_clean normalized_tool_calls.append(tc_clean) clean["tool_calls"] = normalized_tool_calls if "tool_call_id" in clean and clean["tool_call_id"]: clean["tool_call_id"] = map_id(clean["tool_call_id"]) - - if patched_arguments and _should_log_argument_patch(): - logger.info( - "Normalized {} historical tool/function argument payload(s) to JSON strings for LiteLLM", - patched_arguments, - ) return sanitized async def chat( diff --git a/frontend/src/modules/platform/PlatformDashboardPage.tsx b/frontend/src/modules/platform/PlatformDashboardPage.tsx index bdb3909..3e695cd 100644 --- a/frontend/src/modules/platform/PlatformDashboardPage.tsx +++ b/frontend/src/modules/platform/PlatformDashboardPage.tsx @@ -6,6 +6,7 @@ import { ChevronLeft, ChevronRight, Cpu, + Eraser, Eye, ExternalLink, FileText, @@ -152,6 +153,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp const [usageLoading, setUsageLoading] = useState(false); const [usagePage, setUsagePage] = useState(1); const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10)); + const [pageSizeReady, setPageSizeReady] = useState(() => readCachedPlatformPageSize(0) > 0); const [botListPage, setBotListPage] = useState(1); const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10)); const [showCompactBotSheet, setShowCompactBotSheet] = useState(false); @@ -203,6 +205,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); } finally { + setPageSizeReady(true); setLoading(false); } }; @@ -250,8 +253,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp }, []); useEffect(() => { + if (!pageSizeReady) return; void loadUsage(1); - }, [usagePageSize]); + }, [pageSizeReady, usagePageSize]); useEffect(() => { setBotListPage(1); @@ -366,6 +370,31 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp } }; + const clearDashboardDirectSession = async (bot: BotState) => { + const targetId = String(bot.id || '').trim(); + if (!targetId) return; + const ok = await confirm({ + title: isZh ? '清除面板 Session' : 'Clear Dashboard Session', + message: isZh + ? `确认清空 Bot ${targetId} 的 dashboard_direct.jsonl 内容?\n\n这会重置面板对话上下文;若 Bot 正在运行,还会同步切到新会话。` + : `Clear dashboard_direct.jsonl for Bot ${targetId}?\n\nThis resets the dashboard conversation context. If the bot is running, it will also switch to a fresh session.`, + tone: 'warning', + confirmText: isZh ? '清除' : 'Clear', + }); + if (!ok) return; + + setOperatingBotId(targetId); + try { + await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`); + notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' }); + await refreshAll(); + } catch (error: any) { + notify(error?.response?.data?.detail || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' }); + } finally { + setOperatingBotId(''); + } + }; + const loadResourceSnapshot = async (botId: string) => { if (!botId) return; setResourceLoading(true); @@ -391,8 +420,8 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp const overviewImages = overview?.summary.images; const overviewResources = overview?.summary.resources; const usageSummary = usageData?.summary || overview?.usage.summary; - const usageItems = usageData?.items || overview?.usage.items || []; - const usageTotal = usageData?.total || 0; + const usageItems = pageSizeReady ? usageData?.items || [] : []; + const usageTotal = pageSizeReady ? usageData?.total || 0 : 0; const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize)); const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot; const lastActionPreview = selectedBotInfo?.last_action?.trim() || ''; @@ -502,6 +531,16 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp > + void clearDashboardDirectSession(selectedBotInfo)} + tooltip={isZh ? '清除面板 Session' : 'Clear Dashboard Session'} + aria-label={isZh ? '清除面板 Session' : 'Clear Dashboard Session'} + > + + {isZh ? '总计' : 'Total'} {isZh ? '时间 / 来源' : 'Time / Source'} + {!pageSizeReady ? ( +
{isZh ? '正在同步分页设置...' : 'Syncing page size...'}
+ ) : null} {usageItems.map((item) => (
@@ -644,35 +686,37 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
))} - {!usageLoading && usageItems.length === 0 ? ( + {pageSizeReady && !usageLoading && usageItems.length === 0 ? (
{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}
) : null} -
- {isZh ? `第 ${usagePage} / ${usagePageCount} 页,共 ${usageTotal} 条` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`} -
- void loadUsage(usagePage - 1)} - tooltip={isZh ? '上一页' : 'Previous'} - aria-label={isZh ? '上一页' : 'Previous'} - > - - - = usagePageCount} - onClick={() => void loadUsage(usagePage + 1)} - tooltip={isZh ? '下一页' : 'Next'} - aria-label={isZh ? '下一页' : 'Next'} - > - - + {pageSizeReady ? ( +
+ {isZh ? `第 ${usagePage} / ${usagePageCount} 页,共 ${usageTotal} 条` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`} +
+ void loadUsage(usagePage - 1)} + tooltip={isZh ? '上一页' : 'Previous'} + aria-label={isZh ? '上一页' : 'Previous'} + > + + + = usagePageCount} + onClick={() => void loadUsage(usagePage + 1)} + tooltip={isZh ? '下一页' : 'Next'} + aria-label={isZh ? '下一页' : 'Next'} + > + + +
-
+ ) : null} ); @@ -720,7 +764,10 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
- {pagedBots.map((bot) => { + {!pageSizeReady ? ( +
{isZh ? '正在同步分页设置...' : 'Syncing page size...'}
+ ) : null} + {pageSizeReady ? pagedBots.map((bot) => { const enabled = bot.enabled !== false; const running = bot.docker_status === 'RUNNING'; const selected = bot.id === selectedBotId; @@ -772,36 +819,38 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
); - })} - {filteredBots.length === 0 ? ( + }) : null} + {pageSizeReady && filteredBots.length === 0 ? (
{isZh ? '暂无 Bot,或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}
) : null} -
- {isZh ? `第 ${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`} -
- setBotListPage((value) => Math.max(1, value - 1))} - tooltip={isZh ? '上一页' : 'Previous'} - aria-label={isZh ? '上一页' : 'Previous'} - > - - - = botListPageCount} - onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))} - tooltip={isZh ? '下一页' : 'Next'} - aria-label={isZh ? '下一页' : 'Next'} - > - - + {pageSizeReady ? ( +
+ {isZh ? `第 ${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`} +
+ setBotListPage((value) => Math.max(1, value - 1))} + tooltip={isZh ? '上一页' : 'Previous'} + aria-label={isZh ? '上一页' : 'Previous'} + > + + + = botListPageCount} + onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))} + tooltip={isZh ? '下一页' : 'Next'} + aria-label={isZh ? '下一页' : 'Next'} + > + + +
-
+ ) : null} {!compactMode ? (