v0.1.4
parent
5cee6e5e12
commit
c2fe3b8208
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Record<string, 'starting' | 'stopping'>>({});
|
||||
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
||||
const [workspaceEntries, setWorkspaceEntries] = useState<WorkspaceNode[]>([]);
|
||||
const [workspaceSearchEntries, setWorkspaceSearchEntries] = useState<WorkspaceNode[]>([]);
|
||||
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<WorkspacePreviewState | null>(null);
|
||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||
const [workspaceQuery, setWorkspaceQuery] = useState('');
|
||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||
const [quotedReply, setQuotedReply] = useState<QuotedReply | null>(null);
|
||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||
|
|
@ -640,6 +642,37 @@ export function BotDashboardModule({
|
|||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(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<WorkspaceTreeResponse>(`${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({
|
|||
<a
|
||||
key={`${item.ts}-${filePath}`}
|
||||
className="ops-attach-link mono"
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
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<WorkspaceTreeResponse>(`${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<BotChannel[]>(`${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({
|
|||
</div>
|
||||
|
||||
<div className="ops-bot-list-toolbar">
|
||||
<input
|
||||
className="input"
|
||||
value={botListQuery}
|
||||
onChange={(e) => setBotListQuery(e.target.value)}
|
||||
placeholder={t.botSearchPlaceholder}
|
||||
aria-label={t.botSearchPlaceholder}
|
||||
/>
|
||||
<div className="ops-searchbar">
|
||||
<input
|
||||
className="input ops-search-input ops-search-input-with-icon"
|
||||
value={botListQuery}
|
||||
onChange={(e) => setBotListQuery(e.target.value)}
|
||||
placeholder={t.botSearchPlaceholder}
|
||||
aria-label={t.botSearchPlaceholder}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ops-search-inline-btn"
|
||||
onClick={() => {
|
||||
if (botListQuery.trim()) {
|
||||
setBotListQuery('');
|
||||
setBotListPage(1);
|
||||
return;
|
||||
}
|
||||
setBotListPage(1);
|
||||
}}
|
||||
title={botListQuery.trim() ? t.clearSearch : t.searchAction}
|
||||
aria-label={botListQuery.trim() ? t.clearSearch : t.searchAction}
|
||||
>
|
||||
{botListQuery.trim() ? <X size={14} /> : <Search size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list-scroll" style={{ maxHeight: '72vh' }}>
|
||||
<div className="list-scroll">
|
||||
{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);
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<Settings2 size={14} />
|
||||
|
|
@ -3019,7 +3129,11 @@ export function BotDashboardModule({
|
|||
role="menuitem"
|
||||
onClick={() => {
|
||||
setRuntimeMenuOpen(false);
|
||||
setShowParamModal(true);
|
||||
void (async () => {
|
||||
const detail = await ensureSelectedBotDetail();
|
||||
applyEditFormFromBot(detail);
|
||||
setShowParamModal(true);
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
|
|
@ -3080,7 +3194,11 @@ export function BotDashboardModule({
|
|||
role="menuitem"
|
||||
onClick={() => {
|
||||
setRuntimeMenuOpen(false);
|
||||
setShowAgentModal(true);
|
||||
void (async () => {
|
||||
const detail = await ensureSelectedBotDetail();
|
||||
applyEditFormFromBot(detail);
|
||||
setShowAgentModal(true);
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<FileText size={14} />
|
||||
|
|
@ -3161,7 +3279,11 @@ export function BotDashboardModule({
|
|||
<div className="section-mini-title">{t.workspaceOutputs}</div>
|
||||
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
||||
<div className="workspace-toolbar">
|
||||
<span className="workspace-path mono">{workspaceCurrentPath || '/'}</span>
|
||||
<div className="workspace-path-wrap">
|
||||
<div className="workspace-path mono" title={workspacePathDisplay}>
|
||||
{workspacePathDisplay}
|
||||
</div>
|
||||
</div>
|
||||
<div className="workspace-toolbar-actions">
|
||||
<LucentIconButton
|
||||
className="workspace-refresh-icon-btn"
|
||||
|
|
@ -3184,14 +3306,40 @@ export function BotDashboardModule({
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="workspace-search-toolbar">
|
||||
<div className="ops-searchbar">
|
||||
<input
|
||||
className="input ops-search-input ops-search-input-with-icon"
|
||||
value={workspaceQuery}
|
||||
onChange={(e) => setWorkspaceQuery(e.target.value)}
|
||||
placeholder={t.workspaceSearchPlaceholder}
|
||||
aria-label={t.workspaceSearchPlaceholder}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ops-search-inline-btn"
|
||||
onClick={() => {
|
||||
if (workspaceQuery.trim()) {
|
||||
setWorkspaceQuery('');
|
||||
return;
|
||||
}
|
||||
setWorkspaceQuery((v) => v.trim());
|
||||
}}
|
||||
title={workspaceQuery.trim() ? t.clearSearch : t.searchAction}
|
||||
aria-label={workspaceQuery.trim() ? t.clearSearch : t.searchAction}
|
||||
>
|
||||
{workspaceQuery.trim() ? <X size={14} /> : <Search size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="workspace-panel">
|
||||
<div className="workspace-list">
|
||||
{workspaceLoading ? (
|
||||
{workspaceLoading || workspaceSearchLoading ? (
|
||||
<div className="ops-empty-inline">{t.loadingDir}</div>
|
||||
) : renderWorkspaceNodes(workspaceEntries).length === 0 ? (
|
||||
<div className="ops-empty-inline">{t.emptyDir}</div>
|
||||
) : renderWorkspaceNodes(filteredWorkspaceEntries).length === 0 ? (
|
||||
<div className="ops-empty-inline">{normalizedWorkspaceQuery ? t.workspaceSearchNoResult : t.emptyDir}</div>
|
||||
) : (
|
||||
renderWorkspaceNodes(workspaceEntries)
|
||||
renderWorkspaceNodes(filteredWorkspaceEntries)
|
||||
)}
|
||||
</div>
|
||||
<div className="workspace-hint">
|
||||
|
|
@ -3977,6 +4125,12 @@ export function BotDashboardModule({
|
|||
<span className="workspace-entry-info-label">{isZh ? '全称' : 'Name'}</span>
|
||||
<span className="workspace-entry-info-value mono">{workspaceHoverCard.node.name || '-'}</span>
|
||||
</div>
|
||||
<div className="workspace-entry-info-row">
|
||||
<span className="workspace-entry-info-label">{isZh ? '完整路径' : 'Full Path'}</span>
|
||||
<span className="workspace-entry-info-value mono">
|
||||
{`/root/.nanobot/workspace/${String(workspaceHoverCard.node.path || '').replace(/^\/+/, '')}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="workspace-entry-info-row">
|
||||
<span className="workspace-entry-info-label">{isZh ? '创建时间' : 'Created'}</span>
|
||||
<span className="workspace-entry-info-value">{formatWorkspaceTime(workspaceHoverCard.node.ctime, isZh)}</span>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ interface AppStore {
|
|||
setBotEvents: (botId: string, events: BotEvent[]) => void;
|
||||
}
|
||||
|
||||
function preferDefined<T>(incoming: T | undefined, fallback: T | undefined): T | undefined {
|
||||
return incoming !== undefined ? incoming : fallback;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((set) => ({
|
||||
activeBots: {},
|
||||
currentView: 'dashboard',
|
||||
|
|
@ -61,6 +65,26 @@ export const useAppStore = create<AppStore>((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 ?? [],
|
||||
|
|
|
|||
Loading…
Reference in New Issue