diff --git a/bot-images/Dashboard.Dockerfile b/bot-images/Dashboard.Dockerfile index 4fe17d0..2351729 100644 --- a/bot-images/Dashboard.Dockerfile +++ b/bot-images/Dashboard.Dockerfile @@ -3,6 +3,7 @@ ENV PYTHONUNBUFFERED=1 ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 ENV PYTHONIOENCODING=utf-8 +ENV PYTHONPATH=/opt/dashboard-patches${PYTHONPATH:+:${PYTHONPATH}} # 1. 替换 Debian 源为国内镜像 RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \ @@ -19,6 +20,155 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN python -m pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --upgrade \ pip setuptools wheel aiohttp +# 3.1 LiteLLM compatibility patch for DashScope coder/code models. +# DashScope coder models require `tool_calls[*].function.arguments` to be a JSON string. +# Some upstream stacks may replay historical tool calls as dicts / Python-literal strings. +# We patch LiteLLM entrypoints at runtime so old history can still be forwarded safely. +RUN mkdir -p /opt/dashboard-patches && cat > /opt/dashboard-patches/sitecustomize.py <<'PY' +import ast +import copy +import json +import os +import sys +from typing import Any + + +def _log(message: str) -> None: + if str(os.getenv("DASHBOARD_LITELLM_PATCH_VERBOSE") or "").strip().lower() not in {"1", "true", "yes", "on"}: + return + print(f"[dashboard-litellm-patch] {message}", file=sys.stderr, flush=True) + + +def _coerce_json_arguments(value: Any) -> str: + 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) + + +def _sanitize_openai_messages(messages: Any) -> tuple[Any, int]: + if not isinstance(messages, list): + return messages, 0 + try: + cloned = copy.deepcopy(messages) + except Exception: + cloned = list(messages) + + changed = 0 + for message in cloned: + if not isinstance(message, dict): + continue + + tool_calls = message.get("tool_calls") + if isinstance(tool_calls, list): + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + function = tool_call.get("function") + if not isinstance(function, dict): + continue + arguments = function.get("arguments") + normalized = _coerce_json_arguments(arguments) + if arguments != normalized: + function["arguments"] = normalized + changed += 1 + + function_call = message.get("function_call") + if isinstance(function_call, dict): + arguments = function_call.get("arguments") + normalized = _coerce_json_arguments(arguments) + if arguments != normalized: + function_call["arguments"] = normalized + changed += 1 + + return cloned, changed + + +def _patch_litellm() -> None: + try: + import litellm # type: ignore + except Exception as exc: + _log(f"litellm import skipped: {exc}") + return + + def _wrap_sync(fn): + if not callable(fn) or getattr(fn, "_dashboard_litellm_patch", False): + return fn + + def wrapper(*args, **kwargs): + messages = kwargs.get("messages") + normalized_messages, changed = _sanitize_openai_messages(messages) + if changed: + kwargs["messages"] = normalized_messages + _log(f"sanitized {changed} tool/function argument payload(s) before sync completion") + return fn(*args, **kwargs) + + setattr(wrapper, "_dashboard_litellm_patch", True) + return wrapper + + def _wrap_async(fn): + if not callable(fn) or getattr(fn, "_dashboard_litellm_patch", False): + return fn + + async def wrapper(*args, **kwargs): + messages = kwargs.get("messages") + normalized_messages, changed = _sanitize_openai_messages(messages) + if changed: + kwargs["messages"] = normalized_messages + _log(f"sanitized {changed} tool/function argument payload(s) before async completion") + return await fn(*args, **kwargs) + + setattr(wrapper, "_dashboard_litellm_patch", True) + return wrapper + + for attr in ("completion", "completion_with_retries"): + if hasattr(litellm, attr): + setattr(litellm, attr, _wrap_sync(getattr(litellm, attr))) + + for attr in ("acompletion",): + if hasattr(litellm, attr): + setattr(litellm, attr, _wrap_async(getattr(litellm, attr))) + + try: + import litellm.main as litellm_main # type: ignore + except Exception: + litellm_main = None + + if litellm_main is not None: + for attr in ("completion", "completion_with_retries"): + if hasattr(litellm_main, attr): + setattr(litellm_main, attr, _wrap_sync(getattr(litellm_main, attr))) + for attr in ("acompletion",): + if hasattr(litellm_main, attr): + setattr(litellm_main, attr, _wrap_async(getattr(litellm_main, attr))) + + _log("LiteLLM monkey patch installed") + + +_patch_litellm() +PY + WORKDIR /app # 这一步会把您修改好的 nanobot/channels/dashboard.py 一起拷进去 COPY . /app diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts index b74fe35..f534908 100644 --- a/frontend/src/i18n/dashboard.en.ts +++ b/frontend/src/i18n/dashboard.en.ts @@ -72,6 +72,7 @@ export const dashboardEn = { workspaceSearchNoResult: 'No matching files or folders.', searchAction: 'Search', clearSearch: 'Clear search', + syncingPageSize: 'Syncing page size...', paginationPrev: 'Prev', paginationNext: 'Next', paginationPage: (current: number, total: number) => `${current} / ${total}`, diff --git a/frontend/src/i18n/dashboard.zh-cn.ts b/frontend/src/i18n/dashboard.zh-cn.ts index c42e975..1ed0735 100644 --- a/frontend/src/i18n/dashboard.zh-cn.ts +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -72,6 +72,7 @@ export const dashboardZhCn = { workspaceSearchNoResult: '没有匹配的文件或目录。', searchAction: '搜索', clearSearch: '清除搜索', + syncingPageSize: '正在同步分页设置...', paginationPrev: '上一页', paginationNext: '下一页', paginationPage: (current: number, total: number) => `${current} / ${total}`, diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index 6d14c08..4e75e12 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -20,6 +20,11 @@ import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { LucentSelect } from '../../components/lucent/LucentSelect'; import { TopicFeedPanel, type TopicFeedItem, type TopicFeedOption } from './topic/TopicFeedPanel'; +import { + normalizePlatformPageSize, + readCachedPlatformPageSize, + writeCachedPlatformPageSize, +} from '../../utils/platformPageSize'; interface BotDashboardModuleProps { onOpenCreateWizard?: () => void; @@ -1138,7 +1143,10 @@ export function BotDashboardModule({ }); const [uploadMaxMb, setUploadMaxMb] = useState(100); const [allowedAttachmentExtensions, setAllowedAttachmentExtensions] = useState([]); - const [botListPageSize, setBotListPageSize] = useState(10); + const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10)); + const [botListPageSizeReady, setBotListPageSizeReady] = useState( + () => readCachedPlatformPageSize(0) > 0, + ); const [chatPullPageSize, setChatPullPageSize] = useState(60); const [chatHasMore, setChatHasMore] = useState(false); const [chatLoadingMore, setChatLoadingMore] = useState(false); @@ -2153,10 +2161,12 @@ export function BotDashboardModule({ if (Number.isFinite(configured) && configured > 0) { setUploadMaxMb(Math.max(1, Math.floor(configured))); } - const configuredPageSize = Number(res.data?.chat?.page_size); - if (Number.isFinite(configuredPageSize) && configuredPageSize > 0) { - setBotListPageSize(Math.max(1, Math.min(100, Math.floor(configuredPageSize)))); - } + const configuredPageSize = normalizePlatformPageSize( + res.data?.chat?.page_size, + readCachedPlatformPageSize(10), + ); + writeCachedPlatformPageSize(configuredPageSize); + setBotListPageSize(configuredPageSize); setAllowedAttachmentExtensions( parseAllowedAttachmentExtensions(res.data?.workspace?.allowed_attachment_extensions), ); @@ -2183,6 +2193,10 @@ export function BotDashboardModule({ } } catch { // keep default limit + } finally { + if (alive) { + setBotListPageSizeReady(true); + } } }; void loadSystemDefaults(); @@ -4953,150 +4967,157 @@ export function BotDashboardModule({
- {pagedBots.map((bot) => { - const selected = selectedBotId === bot.id; - const controlState = controlStateByBot[bot.id]; - const isOperating = operatingBotId === bot.id; - const isEnabled = bot.enabled !== false; - const isStarting = controlState === 'starting'; - const isStopping = controlState === 'stopping'; - const isEnabling = controlState === 'enabling'; - const isDisabling = controlState === 'disabling'; - const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING'; - return ( -
{ - setSelectedBotId(bot.id); - if (compactMode) setCompactPanelTab('chat'); - }} - > -
-
- setBotListPage((p) => Math.max(1, p - 1))} - disabled={botListPage <= 1} - tooltip={t.paginationPrev} - aria-label={t.paginationPrev} - > - - -
{t.paginationPage(botListPage, botListTotalPages)}
- setBotListPage((p) => Math.min(botListTotalPages, p + 1))} - disabled={botListPage >= botListTotalPages} - tooltip={t.paginationNext} - aria-label={t.paginationNext} - > - - -
+ {botListPageSizeReady ? ( +
+ setBotListPage((p) => Math.max(1, p - 1))} + disabled={botListPage <= 1} + tooltip={t.paginationPrev} + aria-label={t.paginationPrev} + > + + +
{t.paginationPage(botListPage, botListTotalPages)}
+ setBotListPage((p) => Math.min(botListTotalPages, p + 1))} + disabled={botListPage >= botListTotalPages} + tooltip={t.paginationNext} + aria-label={t.paginationNext} + > + + +
+ ) : null} ) : null} diff --git a/frontend/src/modules/platform/PlatformDashboardPage.tsx b/frontend/src/modules/platform/PlatformDashboardPage.tsx index 3436059..bdb3909 100644 --- a/frontend/src/modules/platform/PlatformDashboardPage.tsx +++ b/frontend/src/modules/platform/PlatformDashboardPage.tsx @@ -34,6 +34,11 @@ import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { PlatformSettingsModal } from './components/PlatformSettingsModal'; import { TemplateManagerModal } from './components/TemplateManagerModal'; import { appendPanelAccessPassword } from '../../utils/panelAccess'; +import { + normalizePlatformPageSize, + readCachedPlatformPageSize, + writeCachedPlatformPageSize, +} from '../../utils/platformPageSize'; import '../dashboard/BotDashboardModule.css'; function formatBytes(bytes: number) { @@ -65,12 +70,6 @@ function formatDateTime(value: string | null | undefined, locale: string) { }).format(date); } -function normalizePageSize(value: unknown, fallback = 10) { - const parsed = Number(value); - if (!Number.isFinite(parsed) || parsed <= 0) return fallback; - return Math.max(1, Math.min(100, Math.floor(parsed))); -} - function buildBotPanelHref(botId: string) { const encodedId = encodeURIComponent(String(botId || '').trim()); const params = new URLSearchParams(); @@ -152,9 +151,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp const [usageData, setUsageData] = useState(null); const [usageLoading, setUsageLoading] = useState(false); const [usagePage, setUsagePage] = useState(1); - const [usagePageSize, setUsagePageSize] = useState(10); + const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10)); const [botListPage, setBotListPage] = useState(1); - const [botListPageSize, setBotListPageSize] = useState(10); + const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10)); const [showCompactBotSheet, setShowCompactBotSheet] = useState(false); const [compactSheetClosing, setCompactSheetClosing] = useState(false); const [compactSheetMounted, setCompactSheetMounted] = useState(false); @@ -194,8 +193,13 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/platform/overview`); setOverview(res.data); - setUsagePageSize((prev) => normalizePageSize(res.data?.settings?.page_size, prev)); - setBotListPageSize((prev) => normalizePageSize(res.data?.settings?.page_size, prev)); + const normalizedPageSize = normalizePlatformPageSize( + res.data?.settings?.page_size, + readCachedPlatformPageSize(10), + ); + writeCachedPlatformPageSize(normalizedPageSize); + setUsagePageSize(normalizedPageSize); + setBotListPageSize(normalizedPageSize); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); } finally { @@ -956,8 +960,10 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp onClose={() => setShowPlatformSettings(false)} onSaved={(settings) => { setOverview((prev) => (prev ? { ...prev, settings } : prev)); - setUsagePageSize(normalizePageSize(settings.page_size, 10)); - setBotListPageSize(normalizePageSize(settings.page_size, 10)); + const normalizedPageSize = normalizePlatformPageSize(settings.page_size, 10); + writeCachedPlatformPageSize(normalizedPageSize); + setUsagePageSize(normalizedPageSize); + setBotListPageSize(normalizedPageSize); }} /> {showBotLastActionModal && selectedBotInfo ? ( diff --git a/frontend/src/utils/platformPageSize.ts b/frontend/src/utils/platformPageSize.ts new file mode 100644 index 0000000..38cea69 --- /dev/null +++ b/frontend/src/utils/platformPageSize.ts @@ -0,0 +1,29 @@ +const PLATFORM_PAGE_SIZE_STORAGE_KEY = 'nanobot-platform-page-size'; + +export function normalizePlatformPageSize(value: unknown, fallback = 10) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.max(1, Math.min(100, Math.floor(parsed))); +} + +export function readCachedPlatformPageSize(fallback = 10) { + if (typeof window === 'undefined') return fallback; + try { + const raw = window.localStorage.getItem(PLATFORM_PAGE_SIZE_STORAGE_KEY); + return normalizePlatformPageSize(raw, fallback); + } catch { + return fallback; + } +} + +export function writeCachedPlatformPageSize(value: unknown) { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem( + PLATFORM_PAGE_SIZE_STORAGE_KEY, + String(normalizePlatformPageSize(value, 10)), + ); + } catch { + // ignore localStorage write failures + } +}