From c2fe3b82086665c829aaa734fd2da48b54ad657a Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 9 Mar 2026 17:52:42 +0800 Subject: [PATCH] v0.1.4 --- backend/core/settings.py | 10 +- backend/main.py | 77 ++++- docker-compose.prod.yml | 7 + frontend/src/i18n/dashboard.en.ts | 4 + frontend/src/i18n/dashboard.zh-cn.ts | 4 + .../modules/dashboard/BotDashboardModule.css | 80 ++++- .../modules/dashboard/BotDashboardModule.tsx | 284 ++++++++++++++---- frontend/src/store/appStore.ts | 24 ++ 8 files changed, 410 insertions(+), 80 deletions(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index 03935ab..4a9ee6a 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -8,9 +8,15 @@ from dotenv import load_dotenv BACKEND_ROOT: Final[Path] = Path(__file__).resolve().parents[1] PROJECT_ROOT: Final[Path] = BACKEND_ROOT.parent -# Load backend-local env first, then fallback to project root env. +# Load env files from nearest to broadest scope. +# Priority (high -> low, with override=False preserving existing values): +# 1) process environment +# 2) backend/.env +# 3) project/.env +# 4) backend/.env.prod +# 5) project/.env.prod load_dotenv(BACKEND_ROOT / ".env", override=False) -load_dotenv(PROJECT_ROOT / ".env", override=False) +load_dotenv(PROJECT_ROOT / ".env.prod", override=False) def _env_text(name: str, default: str) -> str: diff --git a/backend/main.py b/backend/main.py index d7fa81d..f84fb1a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -37,6 +37,7 @@ from core.settings import ( PANEL_ACCESS_PASSWORD, PROJECT_ROOT, REDIS_ENABLED, + REDIS_PREFIX, REDIS_URL, SQLITE_MIGRATION_SOURCE, UPLOAD_MAX_MB, @@ -531,6 +532,7 @@ async def on_startup(): print(f"🗄️ 数据库引擎: {DATABASE_ENGINE} (echo={DATABASE_ECHO})") print(f"📁 数据库连接: {DATABASE_URL_DISPLAY}") print(f"🧠 Redis 缓存: {'enabled' if cache.ping() else 'disabled'} ({REDIS_URL if REDIS_ENABLED else 'not configured'})") + print(f"🔐 面板访问密码: {'enabled' if str(PANEL_ACCESS_PASSWORD or '').strip() else 'disabled'}") init_database() _migrate_sqlite_if_needed() cache.delete_prefix("") @@ -581,6 +583,26 @@ def get_health(): raise HTTPException(status_code=503, detail=f"database check failed: {e}") +@app.get("/api/health/cache") +def get_cache_health(): + redis_url = str(REDIS_URL or "").strip() + configured = bool(REDIS_ENABLED and redis_url) + client_enabled = bool(getattr(cache, "enabled", False)) + reachable = bool(cache.ping()) if client_enabled else False + status = "ok" + if configured and not reachable: + status = "degraded" + return { + "status": status, + "cache": { + "configured": configured, + "enabled": client_enabled, + "reachable": reachable, + "prefix": REDIS_PREFIX, + }, + } + + def _config_json_path(bot_id: str) -> str: return os.path.join(_bot_data_root(bot_id), "config.json") @@ -1435,6 +1457,52 @@ def _list_workspace_dir(path: str, root: str) -> List[Dict[str, Any]]: return rows +def _list_workspace_dir_recursive(path: str, root: str) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for walk_root, dirnames, filenames in os.walk(path): + dirnames.sort(key=lambda v: v.lower()) + filenames.sort(key=lambda v: v.lower()) + + for name in dirnames: + if name in {".DS_Store"}: + continue + abs_path = os.path.join(walk_root, name) + rel_path = os.path.relpath(abs_path, root).replace("\\", "/") + stat = os.stat(abs_path) + rows.append( + { + "name": name, + "path": rel_path, + "type": "dir", + "size": None, + "ext": "", + "ctime": _workspace_stat_ctime_iso(stat), + "mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z", + } + ) + + for name in filenames: + if name in {".DS_Store"}: + continue + abs_path = os.path.join(walk_root, name) + rel_path = os.path.relpath(abs_path, root).replace("\\", "/") + stat = os.stat(abs_path) + rows.append( + { + "name": name, + "path": rel_path, + "type": "file", + "size": stat.st_size, + "ext": os.path.splitext(name)[1].lower(), + "ctime": _workspace_stat_ctime_iso(stat), + "mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z", + } + ) + + rows.sort(key=lambda v: (v.get("type") != "dir", str(v.get("path", "")).lower())) + return rows + + @app.get("/api/images", response_model=List[NanobotImage]) def list_images(session: Session = Depends(get_session)): cached = cache.get_json(_cache_key_images()) @@ -2442,7 +2510,12 @@ def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_se @app.get("/api/bots/{bot_id}/workspace/tree") -def get_workspace_tree(bot_id: str, path: Optional[str] = None, session: Session = Depends(get_session)): +def get_workspace_tree( + bot_id: str, + path: Optional[str] = None, + recursive: bool = False, + session: Session = Depends(get_session), +): bot = session.get(BotInstance, bot_id) if not bot: raise HTTPException(status_code=404, detail="Bot not found") @@ -2468,7 +2541,7 @@ def get_workspace_tree(bot_id: str, path: Optional[str] = None, session: Session "root": root, "cwd": cwd, "parent": parent, - "entries": _list_workspace_dir(target, root), + "entries": _list_workspace_dir_recursive(target, root) if recursive else _list_workspace_dir(target, root), } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 453eab6..5d7bf05 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -19,6 +19,13 @@ services: DATA_ROOT: ${HOST_DATA_ROOT} BOTS_WORKSPACE_ROOT: ${HOST_BOTS_WORKSPACE_ROOT} DATABASE_URL: ${DATABASE_URL:-} + REDIS_ENABLED: ${REDIS_ENABLED:-false} + REDIS_URL: ${REDIS_URL:-} + REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot} + REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-60} + PANEL_ACCESS_PASSWORD: ${PANEL_ACCESS_PASSWORD:-} + AUTO_MIGRATE_SQLITE_TO_PRIMARY: ${AUTO_MIGRATE_SQLITE_TO_PRIMARY:-true} + SQLITE_MIGRATION_SOURCE: ${SQLITE_MIGRATION_SOURCE:-} volumes: - /var/run/docker.sock:/var/run/docker.sock - ${HOST_DATA_ROOT}:${HOST_DATA_ROOT} diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts index 2e62d77..1b5ba07 100644 --- a/frontend/src/i18n/dashboard.en.ts +++ b/frontend/src/i18n/dashboard.en.ts @@ -51,6 +51,10 @@ export const dashboardEn = { titleBots: 'Bots', botSearchPlaceholder: 'Search by bot name or ID', botSearchNoResult: 'No matching bots.', + workspaceSearchPlaceholder: 'Search by file name or path', + workspaceSearchNoResult: 'No matching files or folders.', + searchAction: 'Search', + clearSearch: 'Clear search', 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 990f274..e971254 100644 --- a/frontend/src/i18n/dashboard.zh-cn.ts +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -51,6 +51,10 @@ export const dashboardZhCn = { titleBots: 'Bot 列表', botSearchPlaceholder: '按 Bot 名称或 ID 搜索', botSearchNoResult: '没有匹配的 Bot。', + workspaceSearchPlaceholder: '按文件名或路径搜索', + workspaceSearchNoResult: '没有匹配的文件或目录。', + searchAction: '搜索', + clearSearch: '清除搜索', paginationPrev: '上一页', paginationNext: '下一页', paginationPage: (current: number, total: number) => `${current} / ${total}`, diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css index ac2af38..7c42ebc 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.css +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -1,5 +1,16 @@ .ops-bot-list { min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr) auto; + gap: 8px; +} + +.ops-bot-list .list-scroll { + min-height: 0; + overflow: auto; + padding-right: 2px; + max-height: 72vh; } .ops-compact-hidden { @@ -34,6 +45,41 @@ margin-top: 8px; } +.ops-searchbar { + position: relative; + display: block; +} + +.ops-search-input { + min-width: 0; +} + +.ops-search-input-with-icon { + padding-right: 38px; +} + +.ops-search-inline-btn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid transparent; + background: transparent; + color: var(--icon); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.ops-search-inline-btn:hover { + border-color: color-mix(in oklab, var(--brand) 56%, var(--line) 44%); + background: color-mix(in oklab, var(--brand-soft) 42%, var(--panel-soft) 58%); +} + .ops-bot-list-empty { border: 1px dashed var(--line); border-radius: 10px; @@ -46,11 +92,18 @@ } .ops-bot-list-pagination { - margin-top: 8px; + margin-top: 0; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; + position: sticky; + bottom: 0; + z-index: 6; + padding-top: 8px; + border-top: 1px solid color-mix(in oklab, var(--line) 78%, transparent); + background: color-mix(in oklab, var(--panel) 88%, transparent); + backdrop-filter: blur(4px); } .ops-bot-list-page-indicator { @@ -1599,12 +1652,16 @@ .workspace-toolbar { display: flex; gap: 8px; - align-items: center; + align-items: stretch; } -.workspace-path { +.workspace-path-wrap { min-width: 0; flex: 1 1 auto; + border: 1px solid color-mix(in oklab, var(--line) 78%, transparent); + border-radius: 10px; + background: color-mix(in oklab, var(--panel) 58%, var(--panel-soft) 42%); + padding: 8px 10px; } .workspace-toolbar-actions { @@ -1688,19 +1745,18 @@ } .workspace-path { - border: 1px solid var(--line); - border-radius: 8px; - padding: 6px 8px; font-size: 11px; - color: var(--muted); - background: var(--panel-soft); + color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - flex: 1; min-width: 0; } +.workspace-search-toolbar { + margin-top: 8px; +} + .workspace-list { background: var(--panel); overflow: auto; @@ -1736,6 +1792,7 @@ .workspace-entry .workspace-entry-meta { color: var(--muted); font-size: 11px; + margin-top: 1px; } .workspace-entry.dir { @@ -2062,7 +2119,8 @@ } .app-shell[data-theme='light'] .workspace-hint, -.app-shell[data-theme='light'] .workspace-path { +.app-shell[data-theme='light'] .workspace-path, +.app-shell[data-theme='light'] .workspace-path-wrap { background: #f7fbff; } @@ -2089,7 +2147,7 @@ gap: 6px; } - .workspace-path { + .workspace-path-wrap { flex: 1; } diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index 2a23951..afc8dc3 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react'; import axios from 'axios'; -import { Activity, Boxes, Check, ChevronLeft, ChevronRight, Clock3, Copy, Download, EllipsisVertical, ExternalLink, Eye, EyeOff, FileText, FolderOpen, Gauge, Hammer, Maximize2, MessageSquareText, Minimize2, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Reply, Save, Settings2, SlidersHorizontal, Square, ThumbsDown, ThumbsUp, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; +import { Activity, Boxes, Check, ChevronLeft, ChevronRight, Clock3, Copy, Download, EllipsisVertical, ExternalLink, Eye, EyeOff, FileText, FolderOpen, Gauge, Hammer, Maximize2, MessageSquareText, Minimize2, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Reply, Save, Search, Settings2, SlidersHorizontal, Square, ThumbsDown, ThumbsUp, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; @@ -376,6 +376,7 @@ function workspaceFileAction(path: string): 'preview' | 'download' | 'unsupporte } const WORKSPACE_LINK_PREFIX = 'https://workspace.local/open/'; +const WORKSPACE_ABS_PATH_PATTERN = /\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+/gi; function buildWorkspaceLink(path: string) { return `${WORKSPACE_LINK_PREFIX}${encodeURIComponent(path)}`; @@ -403,9 +404,7 @@ function decorateWorkspacePathsForMarkdown(text: string) { return `[${markdownPath}](${buildWorkspaceLink(normalized)})`; }, ); - const workspacePathPattern = - /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi; - return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => { + return normalizedExistingLinks.replace(WORKSPACE_ABS_PATH_PATTERN, (fullPath) => { const normalized = normalizeDashboardAttachmentPath(fullPath); if (!normalized) return fullPath; return `[${fullPath}](${buildWorkspaceLink(normalized)})`; @@ -594,6 +593,8 @@ export function BotDashboardModule({ const [controlStateByBot, setControlStateByBot] = useState>({}); const chatBottomRef = useRef(null); const [workspaceEntries, setWorkspaceEntries] = useState([]); + const [workspaceSearchEntries, setWorkspaceSearchEntries] = useState([]); + const [workspaceSearchLoading, setWorkspaceSearchLoading] = useState(false); const [workspaceLoading, setWorkspaceLoading] = useState(false); const [workspaceError, setWorkspaceError] = useState(''); const [workspaceCurrentPath, setWorkspaceCurrentPath] = useState(''); @@ -602,6 +603,7 @@ export function BotDashboardModule({ const [workspacePreview, setWorkspacePreview] = useState(null); const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false); const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false); + const [workspaceQuery, setWorkspaceQuery] = useState(''); const [pendingAttachments, setPendingAttachments] = useState([]); const [quotedReply, setQuotedReply] = useState(null); const [isUploadingAttachments, setIsUploadingAttachments] = useState(false); @@ -640,6 +642,37 @@ export function BotDashboardModule({ const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false); const [workspaceHoverCard, setWorkspaceHoverCard] = useState(null); const runtimeMenuRef = useRef(null); + const applyEditFormFromBot = useCallback((bot?: any) => { + if (!bot) return; + setProviderTestResult(''); + setEditForm({ + name: bot.name || '', + access_password: bot.access_password || '', + llm_provider: bot.llm_provider || 'dashscope', + llm_model: bot.llm_model || '', + image_tag: bot.image_tag || '', + api_key: '', + api_base: bot.api_base || '', + temperature: clampTemperature(bot.temperature ?? 0.2), + top_p: bot.top_p ?? 1, + max_tokens: clampMaxTokens(bot.max_tokens ?? 8192), + cpu_cores: clampCpuCores(bot.cpu_cores ?? 1), + memory_mb: clampMemoryMb(bot.memory_mb ?? 1024), + storage_gb: clampStorageGb(bot.storage_gb ?? 10), + agents_md: bot.agents_md || '', + soul_md: bot.soul_md || bot.system_prompt || '', + user_md: bot.user_md || '', + tools_md: bot.tools_md || '', + identity_md: bot.identity_md || '', + }); + setParamDraft({ + max_tokens: String(clampMaxTokens(bot.max_tokens ?? 8192)), + cpu_cores: String(clampCpuCores(bot.cpu_cores ?? 1)), + memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)), + storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)), + }); + setPendingAttachments([]); + }, []); const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => { const accessPassword = selectedBotId ? getBotAccessPassword(selectedBotId) : ''; const panelPassword = getPanelAccessPassword(); @@ -712,7 +745,7 @@ export function BotDashboardModule({ notify(failMsg, { tone: 'error' }); } }; - const openWorkspacePathFromChat = (path: string) => { + const openWorkspacePathFromChat = async (path: string) => { const normalized = String(path || '').trim(); if (!normalized) return; const action = workspaceFileAction(normalized); @@ -724,16 +757,24 @@ export function BotDashboardModule({ void openWorkspaceFilePreview(normalized); return; } - if (!isPreviewableWorkspacePath(normalized) || action === 'unsupported') { - notify(fileNotPreviewableLabel, { tone: 'warning' }); + try { + await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/tree`, { + params: { path: normalized }, + }); + await loadWorkspaceTree(selectedBotId, normalized); return; + } catch { + if (!isPreviewableWorkspacePath(normalized) || action === 'unsupported') { + notify(fileNotPreviewableLabel, { tone: 'warning' }); + return; + } } }; const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => { const source = String(text || ''); if (!source) return [source]; const pattern = - /\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))\]\((https:\/\/workspace\.local\/open\/[^)\r\n]*)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|html|htm|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b|https:\/\/workspace\.local\/open\/[^)\r\n]+/gi; + /\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\r\n]*)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+|https:\/\/workspace\.local\/open\/[^)\r\n]+/gi; const nodes: ReactNode[] = []; let lastIndex = 0; let matchIndex = 0; @@ -768,7 +809,7 @@ export function BotDashboardModule({ onClick={(event) => { event.preventDefault(); event.stopPropagation(); - openWorkspacePathFromChat(normalizedPath); + void openWorkspacePathFromChat(normalizedPath); }} > {displayText} @@ -809,7 +850,7 @@ export function BotDashboardModule({ href="#" onClick={(event) => { event.preventDefault(); - openWorkspacePathFromChat(workspacePath); + void openWorkspacePathFromChat(workspacePath); }} {...props} > @@ -1040,6 +1081,19 @@ export function BotDashboardModule({ () => workspaceEntries.filter((v) => v.type === 'file' && isPreviewableWorkspaceFile(v)), [workspaceEntries], ); + const workspacePathDisplay = workspaceCurrentPath + ? `/${String(workspaceCurrentPath || '').replace(/^\/+/, '')}` + : '/'; + const normalizedWorkspaceQuery = workspaceQuery.trim().toLowerCase(); + const filteredWorkspaceEntries = useMemo(() => { + const sourceEntries = normalizedWorkspaceQuery ? workspaceSearchEntries : workspaceEntries; + if (!normalizedWorkspaceQuery) return sourceEntries; + return sourceEntries.filter((entry) => { + const name = String(entry.name || '').toLowerCase(); + const path = String(entry.path || '').toLowerCase(); + return name.includes(normalizedWorkspaceQuery) || path.includes(normalizedWorkspaceQuery); + }); + }, [workspaceEntries, workspaceSearchEntries, normalizedWorkspaceQuery]); const addableChannelTypes = useMemo(() => { const exists = new Set(channels.map((c) => String(c.channel_type).toLowerCase())); return optionalChannelTypes.filter((t) => !exists.has(t)); @@ -1236,11 +1290,11 @@ export function BotDashboardModule({ { - event.preventDefault(); - openWorkspacePathFromChat(filePath); - }} + href="#" + onClick={(event) => { + event.preventDefault(); + void openWorkspacePathFromChat(filePath); + }} title={fileAction === 'download' ? t.download : fileAction === 'preview' ? t.previewTitle : t.fileNotPreviewable} > {fileAction === 'download' ? ( @@ -1435,37 +1489,17 @@ export function BotDashboardModule({ useEffect(() => { if (!selectedBotId) return; - const bot = selectedBot; - if (!bot) return; - setProviderTestResult(''); - setEditForm({ - name: bot.name || '', - access_password: bot.access_password || '', - llm_provider: bot.llm_provider || 'dashscope', - llm_model: bot.llm_model || '', - image_tag: bot.image_tag || '', - api_key: '', - api_base: bot.api_base || '', - temperature: clampTemperature(bot.temperature ?? 0.2), - top_p: bot.top_p ?? 1, - max_tokens: clampMaxTokens(bot.max_tokens ?? 8192), - cpu_cores: clampCpuCores(bot.cpu_cores ?? 1), - memory_mb: clampMemoryMb(bot.memory_mb ?? 1024), - storage_gb: clampStorageGb(bot.storage_gb ?? 10), - agents_md: bot.agents_md || '', - soul_md: bot.soul_md || bot.system_prompt || '', - user_md: bot.user_md || '', - tools_md: bot.tools_md || '', - identity_md: bot.identity_md || '', - }); - setParamDraft({ - max_tokens: String(clampMaxTokens(bot.max_tokens ?? 8192)), - cpu_cores: String(clampCpuCores(bot.cpu_cores ?? 1)), - memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)), - storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)), - }); - setPendingAttachments([]); - }, [selectedBotId, selectedBot?.id]); + if (showBaseModal || showParamModal || showAgentModal) return; + applyEditFormFromBot(selectedBot); + }, [ + selectedBotId, + selectedBot?.id, + selectedBot?.updated_at, + showBaseModal, + showParamModal, + showAgentModal, + applyEditFormFromBot, + ]); useEffect(() => { if (!selectedBotId || !selectedBot) { @@ -1501,6 +1535,17 @@ export function BotDashboardModule({ await loadImageOptions(); }; + const ensureSelectedBotDetail = useCallback(async () => { + if (!selectedBotId) return selectedBot; + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}`); + mergeBot(res.data); + return res.data; + } catch { + return selectedBot; + } + }, [selectedBotId, selectedBot, mergeBot]); + const loadResourceSnapshot = async (botId: string) => { if (!botId) return; setResourceLoading(true); @@ -1626,10 +1671,12 @@ export function BotDashboardModule({ }); const entries = Array.isArray(res.data?.entries) ? res.data.entries : []; setWorkspaceEntries(entries); + setWorkspaceSearchEntries([]); setWorkspaceCurrentPath(res.data?.cwd || ''); setWorkspaceParentPath(res.data?.parent ?? null); } catch (error: any) { setWorkspaceEntries([]); + setWorkspaceSearchEntries([]); setWorkspaceCurrentPath(''); setWorkspaceParentPath(null); setWorkspaceError(error?.response?.data?.detail || t.workspaceLoadFail); @@ -1638,6 +1685,28 @@ export function BotDashboardModule({ } }; + const loadWorkspaceSearchEntries = async (botId: string, path: string = '') => { + if (!botId) return; + const q = String(workspaceQuery || '').trim(); + if (!q) { + setWorkspaceSearchEntries([]); + setWorkspaceSearchLoading(false); + return; + } + setWorkspaceSearchLoading(true); + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/workspace/tree`, { + params: { path, recursive: true }, + }); + const entries = Array.isArray(res.data?.entries) ? res.data.entries : []; + setWorkspaceSearchEntries(entries); + } catch { + setWorkspaceSearchEntries([]); + } finally { + setWorkspaceSearchLoading(false); + } + }; + const loadChannels = async (botId: string) => { if (!botId) return; const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`); @@ -2386,6 +2455,25 @@ export function BotDashboardModule({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]); + useEffect(() => { + setWorkspaceQuery(''); + }, [selectedBotId, workspaceCurrentPath]); + + useEffect(() => { + if (!selectedBotId) { + setWorkspaceSearchEntries([]); + setWorkspaceSearchLoading(false); + return; + } + if (!workspaceQuery.trim()) { + setWorkspaceSearchEntries([]); + setWorkspaceSearchLoading(false); + return; + } + void loadWorkspaceSearchEntries(selectedBotId, workspaceCurrentPath); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedBotId, workspaceCurrentPath, workspaceQuery]); + const saveBot = async (mode: 'params' | 'agent' | 'base') => { const targetBotId = String(selectedBot?.id || selectedBotId || '').trim(); if (!targetBotId) { @@ -2649,16 +2737,34 @@ export function BotDashboardModule({
- setBotListQuery(e.target.value)} - placeholder={t.botSearchPlaceholder} - aria-label={t.botSearchPlaceholder} - /> +
+ setBotListQuery(e.target.value)} + placeholder={t.botSearchPlaceholder} + aria-label={t.botSearchPlaceholder} + /> + +
-
+
{pagedBots.map((bot) => { const selected = selectedBotId === bot.id; const controlState = controlStateByBot[bot.id]; @@ -2857,7 +2963,7 @@ export function BotDashboardModule({ onClick={(event) => { event.preventDefault(); event.stopPropagation(); - openWorkspacePathFromChat(filePath); + void openWorkspacePathFromChat(filePath); }} > {fileAction === 'download' ? ( @@ -3007,8 +3113,12 @@ export function BotDashboardModule({ role="menuitem" onClick={() => { setRuntimeMenuOpen(false); - setProviderTestResult(''); - setShowBaseModal(true); + void (async () => { + const detail = await ensureSelectedBotDetail(); + applyEditFormFromBot(detail); + setProviderTestResult(''); + setShowBaseModal(true); + })(); }} > @@ -3019,7 +3129,11 @@ export function BotDashboardModule({ role="menuitem" onClick={() => { setRuntimeMenuOpen(false); - setShowParamModal(true); + void (async () => { + const detail = await ensureSelectedBotDetail(); + applyEditFormFromBot(detail); + setShowParamModal(true); + })(); }} > @@ -3080,7 +3194,11 @@ export function BotDashboardModule({ role="menuitem" onClick={() => { setRuntimeMenuOpen(false); - setShowAgentModal(true); + void (async () => { + const detail = await ensureSelectedBotDetail(); + applyEditFormFromBot(detail); + setShowAgentModal(true); + })(); }} > @@ -3161,7 +3279,11 @@ export function BotDashboardModule({
{t.workspaceOutputs}
{workspaceError ?
{workspaceError}
: null}
- {workspaceCurrentPath || '/'} +
+
+ {workspacePathDisplay} +
+
+
+
+ setWorkspaceQuery(e.target.value)} + placeholder={t.workspaceSearchPlaceholder} + aria-label={t.workspaceSearchPlaceholder} + /> + +
+
- {workspaceLoading ? ( + {workspaceLoading || workspaceSearchLoading ? (
{t.loadingDir}
- ) : renderWorkspaceNodes(workspaceEntries).length === 0 ? ( -
{t.emptyDir}
+ ) : renderWorkspaceNodes(filteredWorkspaceEntries).length === 0 ? ( +
{normalizedWorkspaceQuery ? t.workspaceSearchNoResult : t.emptyDir}
) : ( - renderWorkspaceNodes(workspaceEntries) + renderWorkspaceNodes(filteredWorkspaceEntries) )}
@@ -3977,6 +4125,12 @@ export function BotDashboardModule({ {isZh ? '全称' : 'Name'} {workspaceHoverCard.node.name || '-'}
+
+ {isZh ? '完整路径' : 'Full Path'} + + {`/root/.nanobot/workspace/${String(workspaceHoverCard.node.path || '').replace(/^\/+/, '')}`} + +
{isZh ? '创建时间' : 'Created'} {formatWorkspaceTime(workspaceHoverCard.node.ctime, isZh)} diff --git a/frontend/src/store/appStore.ts b/frontend/src/store/appStore.ts index 03263c8..f147a69 100644 --- a/frontend/src/store/appStore.ts +++ b/frontend/src/store/appStore.ts @@ -37,6 +37,10 @@ interface AppStore { setBotEvents: (botId: string, events: BotEvent[]) => void; } +function preferDefined(incoming: T | undefined, fallback: T | undefined): T | undefined { + return incoming !== undefined ? incoming : fallback; +} + export const useAppStore = create((set) => ({ activeBots: {}, currentView: 'dashboard', @@ -61,6 +65,26 @@ export const useAppStore = create((set) => ({ nextBots[bot.id] = { ...prev, ...bot, + image_tag: preferDefined(bot.image_tag, prev?.image_tag), + llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider), + llm_model: preferDefined(bot.llm_model, prev?.llm_model), + api_base: preferDefined(bot.api_base, prev?.api_base), + temperature: preferDefined(bot.temperature, prev?.temperature), + top_p: preferDefined(bot.top_p, prev?.top_p), + max_tokens: preferDefined(bot.max_tokens, prev?.max_tokens), + cpu_cores: preferDefined(bot.cpu_cores, prev?.cpu_cores), + memory_mb: preferDefined(bot.memory_mb, prev?.memory_mb), + storage_gb: preferDefined(bot.storage_gb, prev?.storage_gb), + send_progress: preferDefined(bot.send_progress, prev?.send_progress), + send_tool_hints: preferDefined(bot.send_tool_hints, prev?.send_tool_hints), + soul_md: preferDefined(bot.soul_md, prev?.soul_md), + agents_md: preferDefined(bot.agents_md, prev?.agents_md), + user_md: preferDefined(bot.user_md, prev?.user_md), + tools_md: preferDefined(bot.tools_md, prev?.tools_md), + identity_md: preferDefined(bot.identity_md, prev?.identity_md), + system_prompt: preferDefined(bot.system_prompt, prev?.system_prompt), + access_password: preferDefined(bot.access_password, prev?.access_password), + has_access_password: preferDefined(bot.has_access_password, prev?.has_access_password), logs: prev?.logs ?? [], messages: prev?.messages ?? [], events: prev?.events ?? [],