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]
|
BACKEND_ROOT: Final[Path] = Path(__file__).resolve().parents[1]
|
||||||
PROJECT_ROOT: Final[Path] = BACKEND_ROOT.parent
|
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(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:
|
def _env_text(name: str, default: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ from core.settings import (
|
||||||
PANEL_ACCESS_PASSWORD,
|
PANEL_ACCESS_PASSWORD,
|
||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
REDIS_ENABLED,
|
REDIS_ENABLED,
|
||||||
|
REDIS_PREFIX,
|
||||||
REDIS_URL,
|
REDIS_URL,
|
||||||
SQLITE_MIGRATION_SOURCE,
|
SQLITE_MIGRATION_SOURCE,
|
||||||
UPLOAD_MAX_MB,
|
UPLOAD_MAX_MB,
|
||||||
|
|
@ -531,6 +532,7 @@ async def on_startup():
|
||||||
print(f"🗄️ 数据库引擎: {DATABASE_ENGINE} (echo={DATABASE_ECHO})")
|
print(f"🗄️ 数据库引擎: {DATABASE_ENGINE} (echo={DATABASE_ECHO})")
|
||||||
print(f"📁 数据库连接: {DATABASE_URL_DISPLAY}")
|
print(f"📁 数据库连接: {DATABASE_URL_DISPLAY}")
|
||||||
print(f"🧠 Redis 缓存: {'enabled' if cache.ping() else 'disabled'} ({REDIS_URL if REDIS_ENABLED else 'not configured'})")
|
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()
|
init_database()
|
||||||
_migrate_sqlite_if_needed()
|
_migrate_sqlite_if_needed()
|
||||||
cache.delete_prefix("")
|
cache.delete_prefix("")
|
||||||
|
|
@ -581,6 +583,26 @@ def get_health():
|
||||||
raise HTTPException(status_code=503, detail=f"database check failed: {e}")
|
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:
|
def _config_json_path(bot_id: str) -> str:
|
||||||
return os.path.join(_bot_data_root(bot_id), "config.json")
|
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
|
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])
|
@app.get("/api/images", response_model=List[NanobotImage])
|
||||||
def list_images(session: Session = Depends(get_session)):
|
def list_images(session: Session = Depends(get_session)):
|
||||||
cached = cache.get_json(_cache_key_images())
|
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")
|
@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)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
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,
|
"root": root,
|
||||||
"cwd": cwd,
|
"cwd": cwd,
|
||||||
"parent": parent,
|
"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}
|
DATA_ROOT: ${HOST_DATA_ROOT}
|
||||||
BOTS_WORKSPACE_ROOT: ${HOST_BOTS_WORKSPACE_ROOT}
|
BOTS_WORKSPACE_ROOT: ${HOST_BOTS_WORKSPACE_ROOT}
|
||||||
DATABASE_URL: ${DATABASE_URL:-}
|
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:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ${HOST_DATA_ROOT}:${HOST_DATA_ROOT}
|
- ${HOST_DATA_ROOT}:${HOST_DATA_ROOT}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ export const dashboardEn = {
|
||||||
titleBots: 'Bots',
|
titleBots: 'Bots',
|
||||||
botSearchPlaceholder: 'Search by bot name or ID',
|
botSearchPlaceholder: 'Search by bot name or ID',
|
||||||
botSearchNoResult: 'No matching bots.',
|
botSearchNoResult: 'No matching bots.',
|
||||||
|
workspaceSearchPlaceholder: 'Search by file name or path',
|
||||||
|
workspaceSearchNoResult: 'No matching files or folders.',
|
||||||
|
searchAction: 'Search',
|
||||||
|
clearSearch: 'Clear search',
|
||||||
paginationPrev: 'Prev',
|
paginationPrev: 'Prev',
|
||||||
paginationNext: 'Next',
|
paginationNext: 'Next',
|
||||||
paginationPage: (current: number, total: number) => `${current} / ${total}`,
|
paginationPage: (current: number, total: number) => `${current} / ${total}`,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ export const dashboardZhCn = {
|
||||||
titleBots: 'Bot 列表',
|
titleBots: 'Bot 列表',
|
||||||
botSearchPlaceholder: '按 Bot 名称或 ID 搜索',
|
botSearchPlaceholder: '按 Bot 名称或 ID 搜索',
|
||||||
botSearchNoResult: '没有匹配的 Bot。',
|
botSearchNoResult: '没有匹配的 Bot。',
|
||||||
|
workspaceSearchPlaceholder: '按文件名或路径搜索',
|
||||||
|
workspaceSearchNoResult: '没有匹配的文件或目录。',
|
||||||
|
searchAction: '搜索',
|
||||||
|
clearSearch: '清除搜索',
|
||||||
paginationPrev: '上一页',
|
paginationPrev: '上一页',
|
||||||
paginationNext: '下一页',
|
paginationNext: '下一页',
|
||||||
paginationPage: (current: number, total: number) => `${current} / ${total}`,
|
paginationPage: (current: number, total: number) => `${current} / ${total}`,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
.ops-bot-list {
|
.ops-bot-list {
|
||||||
min-width: 0;
|
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 {
|
.ops-compact-hidden {
|
||||||
|
|
@ -34,6 +45,41 @@
|
||||||
margin-top: 8px;
|
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 {
|
.ops-bot-list-empty {
|
||||||
border: 1px dashed var(--line);
|
border: 1px dashed var(--line);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
@ -46,11 +92,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-list-pagination {
|
.ops-bot-list-pagination {
|
||||||
margin-top: 8px;
|
margin-top: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.ops-bot-list-page-indicator {
|
||||||
|
|
@ -1599,12 +1652,16 @@
|
||||||
.workspace-toolbar {
|
.workspace-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-path {
|
.workspace-path-wrap {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
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 {
|
.workspace-toolbar-actions {
|
||||||
|
|
@ -1688,19 +1745,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-path {
|
.workspace-path {
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--text);
|
||||||
background: var(--panel-soft);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-search-toolbar {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-list {
|
.workspace-list {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
@ -1736,6 +1792,7 @@
|
||||||
.workspace-entry .workspace-entry-meta {
|
.workspace-entry .workspace-entry-meta {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry.dir {
|
.workspace-entry.dir {
|
||||||
|
|
@ -2062,7 +2119,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell[data-theme='light'] .workspace-hint,
|
.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;
|
background: #f7fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2089,7 +2147,7 @@
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-path {
|
.workspace-path-wrap {
|
||||||
flex: 1;
|
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 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 ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
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_LINK_PREFIX = 'https://workspace.local/open/';
|
||||||
|
const WORKSPACE_ABS_PATH_PATTERN = /\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+/gi;
|
||||||
|
|
||||||
function buildWorkspaceLink(path: string) {
|
function buildWorkspaceLink(path: string) {
|
||||||
return `${WORKSPACE_LINK_PREFIX}${encodeURIComponent(path)}`;
|
return `${WORKSPACE_LINK_PREFIX}${encodeURIComponent(path)}`;
|
||||||
|
|
@ -403,9 +404,7 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
||||||
return `[${markdownPath}](${buildWorkspaceLink(normalized)})`;
|
return `[${markdownPath}](${buildWorkspaceLink(normalized)})`;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const workspacePathPattern =
|
return normalizedExistingLinks.replace(WORKSPACE_ABS_PATH_PATTERN, (fullPath) => {
|
||||||
/\/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) => {
|
|
||||||
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||||
if (!normalized) return fullPath;
|
if (!normalized) return fullPath;
|
||||||
return `[${fullPath}](${buildWorkspaceLink(normalized)})`;
|
return `[${fullPath}](${buildWorkspaceLink(normalized)})`;
|
||||||
|
|
@ -594,6 +593,8 @@ export function BotDashboardModule({
|
||||||
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping'>>({});
|
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping'>>({});
|
||||||
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [workspaceEntries, setWorkspaceEntries] = useState<WorkspaceNode[]>([]);
|
const [workspaceEntries, setWorkspaceEntries] = useState<WorkspaceNode[]>([]);
|
||||||
|
const [workspaceSearchEntries, setWorkspaceSearchEntries] = useState<WorkspaceNode[]>([]);
|
||||||
|
const [workspaceSearchLoading, setWorkspaceSearchLoading] = useState(false);
|
||||||
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
||||||
const [workspaceError, setWorkspaceError] = useState('');
|
const [workspaceError, setWorkspaceError] = useState('');
|
||||||
const [workspaceCurrentPath, setWorkspaceCurrentPath] = useState('');
|
const [workspaceCurrentPath, setWorkspaceCurrentPath] = useState('');
|
||||||
|
|
@ -602,6 +603,7 @@ export function BotDashboardModule({
|
||||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||||
|
const [workspaceQuery, setWorkspaceQuery] = useState('');
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||||
const [quotedReply, setQuotedReply] = useState<QuotedReply | null>(null);
|
const [quotedReply, setQuotedReply] = useState<QuotedReply | null>(null);
|
||||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||||
|
|
@ -640,6 +642,37 @@ export function BotDashboardModule({
|
||||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | 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 buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => {
|
||||||
const accessPassword = selectedBotId ? getBotAccessPassword(selectedBotId) : '';
|
const accessPassword = selectedBotId ? getBotAccessPassword(selectedBotId) : '';
|
||||||
const panelPassword = getPanelAccessPassword();
|
const panelPassword = getPanelAccessPassword();
|
||||||
|
|
@ -712,7 +745,7 @@ export function BotDashboardModule({
|
||||||
notify(failMsg, { tone: 'error' });
|
notify(failMsg, { tone: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const openWorkspacePathFromChat = (path: string) => {
|
const openWorkspacePathFromChat = async (path: string) => {
|
||||||
const normalized = String(path || '').trim();
|
const normalized = String(path || '').trim();
|
||||||
if (!normalized) return;
|
if (!normalized) return;
|
||||||
const action = workspaceFileAction(normalized);
|
const action = workspaceFileAction(normalized);
|
||||||
|
|
@ -724,16 +757,24 @@ export function BotDashboardModule({
|
||||||
void openWorkspaceFilePreview(normalized);
|
void openWorkspaceFilePreview(normalized);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isPreviewableWorkspacePath(normalized) || action === 'unsupported') {
|
try {
|
||||||
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
await axios.get<WorkspaceTreeResponse>(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/tree`, {
|
||||||
|
params: { path: normalized },
|
||||||
|
});
|
||||||
|
await loadWorkspaceTree(selectedBotId, normalized);
|
||||||
return;
|
return;
|
||||||
|
} catch {
|
||||||
|
if (!isPreviewableWorkspacePath(normalized) || action === 'unsupported') {
|
||||||
|
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
if (!source) return [source];
|
if (!source) return [source];
|
||||||
const pattern =
|
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[] = [];
|
const nodes: ReactNode[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let matchIndex = 0;
|
let matchIndex = 0;
|
||||||
|
|
@ -768,7 +809,7 @@ export function BotDashboardModule({
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openWorkspacePathFromChat(normalizedPath);
|
void openWorkspacePathFromChat(normalizedPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayText}
|
{displayText}
|
||||||
|
|
@ -809,7 +850,7 @@ export function BotDashboardModule({
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openWorkspacePathFromChat(workspacePath);
|
void openWorkspacePathFromChat(workspacePath);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -1040,6 +1081,19 @@ export function BotDashboardModule({
|
||||||
() => workspaceEntries.filter((v) => v.type === 'file' && isPreviewableWorkspaceFile(v)),
|
() => workspaceEntries.filter((v) => v.type === 'file' && isPreviewableWorkspaceFile(v)),
|
||||||
[workspaceEntries],
|
[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 addableChannelTypes = useMemo(() => {
|
||||||
const exists = new Set(channels.map((c) => String(c.channel_type).toLowerCase()));
|
const exists = new Set(channels.map((c) => String(c.channel_type).toLowerCase()));
|
||||||
return optionalChannelTypes.filter((t) => !exists.has(t));
|
return optionalChannelTypes.filter((t) => !exists.has(t));
|
||||||
|
|
@ -1236,11 +1290,11 @@ export function BotDashboardModule({
|
||||||
<a
|
<a
|
||||||
key={`${item.ts}-${filePath}`}
|
key={`${item.ts}-${filePath}`}
|
||||||
className="ops-attach-link mono"
|
className="ops-attach-link mono"
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openWorkspacePathFromChat(filePath);
|
void openWorkspacePathFromChat(filePath);
|
||||||
}}
|
}}
|
||||||
title={fileAction === 'download' ? t.download : fileAction === 'preview' ? t.previewTitle : t.fileNotPreviewable}
|
title={fileAction === 'download' ? t.download : fileAction === 'preview' ? t.previewTitle : t.fileNotPreviewable}
|
||||||
>
|
>
|
||||||
{fileAction === 'download' ? (
|
{fileAction === 'download' ? (
|
||||||
|
|
@ -1435,37 +1489,17 @@ export function BotDashboardModule({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedBotId) return;
|
if (!selectedBotId) return;
|
||||||
const bot = selectedBot;
|
if (showBaseModal || showParamModal || showAgentModal) return;
|
||||||
if (!bot) return;
|
applyEditFormFromBot(selectedBot);
|
||||||
setProviderTestResult('');
|
}, [
|
||||||
setEditForm({
|
selectedBotId,
|
||||||
name: bot.name || '',
|
selectedBot?.id,
|
||||||
access_password: bot.access_password || '',
|
selectedBot?.updated_at,
|
||||||
llm_provider: bot.llm_provider || 'dashscope',
|
showBaseModal,
|
||||||
llm_model: bot.llm_model || '',
|
showParamModal,
|
||||||
image_tag: bot.image_tag || '',
|
showAgentModal,
|
||||||
api_key: '',
|
applyEditFormFromBot,
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedBotId || !selectedBot) {
|
if (!selectedBotId || !selectedBot) {
|
||||||
|
|
@ -1501,6 +1535,17 @@ export function BotDashboardModule({
|
||||||
await loadImageOptions();
|
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) => {
|
const loadResourceSnapshot = async (botId: string) => {
|
||||||
if (!botId) return;
|
if (!botId) return;
|
||||||
setResourceLoading(true);
|
setResourceLoading(true);
|
||||||
|
|
@ -1626,10 +1671,12 @@ export function BotDashboardModule({
|
||||||
});
|
});
|
||||||
const entries = Array.isArray(res.data?.entries) ? res.data.entries : [];
|
const entries = Array.isArray(res.data?.entries) ? res.data.entries : [];
|
||||||
setWorkspaceEntries(entries);
|
setWorkspaceEntries(entries);
|
||||||
|
setWorkspaceSearchEntries([]);
|
||||||
setWorkspaceCurrentPath(res.data?.cwd || '');
|
setWorkspaceCurrentPath(res.data?.cwd || '');
|
||||||
setWorkspaceParentPath(res.data?.parent ?? null);
|
setWorkspaceParentPath(res.data?.parent ?? null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setWorkspaceEntries([]);
|
setWorkspaceEntries([]);
|
||||||
|
setWorkspaceSearchEntries([]);
|
||||||
setWorkspaceCurrentPath('');
|
setWorkspaceCurrentPath('');
|
||||||
setWorkspaceParentPath(null);
|
setWorkspaceParentPath(null);
|
||||||
setWorkspaceError(error?.response?.data?.detail || t.workspaceLoadFail);
|
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) => {
|
const loadChannels = async (botId: string) => {
|
||||||
if (!botId) return;
|
if (!botId) return;
|
||||||
const res = await axios.get<BotChannel[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`);
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]);
|
}, [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 saveBot = async (mode: 'params' | 'agent' | 'base') => {
|
||||||
const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();
|
const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();
|
||||||
if (!targetBotId) {
|
if (!targetBotId) {
|
||||||
|
|
@ -2649,16 +2737,34 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ops-bot-list-toolbar">
|
<div className="ops-bot-list-toolbar">
|
||||||
<input
|
<div className="ops-searchbar">
|
||||||
className="input"
|
<input
|
||||||
value={botListQuery}
|
className="input ops-search-input ops-search-input-with-icon"
|
||||||
onChange={(e) => setBotListQuery(e.target.value)}
|
value={botListQuery}
|
||||||
placeholder={t.botSearchPlaceholder}
|
onChange={(e) => setBotListQuery(e.target.value)}
|
||||||
aria-label={t.botSearchPlaceholder}
|
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>
|
||||||
|
|
||||||
<div className="list-scroll" style={{ maxHeight: '72vh' }}>
|
<div className="list-scroll">
|
||||||
{pagedBots.map((bot) => {
|
{pagedBots.map((bot) => {
|
||||||
const selected = selectedBotId === bot.id;
|
const selected = selectedBotId === bot.id;
|
||||||
const controlState = controlStateByBot[bot.id];
|
const controlState = controlStateByBot[bot.id];
|
||||||
|
|
@ -2857,7 +2963,7 @@ export function BotDashboardModule({
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openWorkspacePathFromChat(filePath);
|
void openWorkspacePathFromChat(filePath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fileAction === 'download' ? (
|
{fileAction === 'download' ? (
|
||||||
|
|
@ -3007,8 +3113,12 @@ export function BotDashboardModule({
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRuntimeMenuOpen(false);
|
setRuntimeMenuOpen(false);
|
||||||
setProviderTestResult('');
|
void (async () => {
|
||||||
setShowBaseModal(true);
|
const detail = await ensureSelectedBotDetail();
|
||||||
|
applyEditFormFromBot(detail);
|
||||||
|
setProviderTestResult('');
|
||||||
|
setShowBaseModal(true);
|
||||||
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings2 size={14} />
|
<Settings2 size={14} />
|
||||||
|
|
@ -3019,7 +3129,11 @@ export function BotDashboardModule({
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRuntimeMenuOpen(false);
|
setRuntimeMenuOpen(false);
|
||||||
setShowParamModal(true);
|
void (async () => {
|
||||||
|
const detail = await ensureSelectedBotDetail();
|
||||||
|
applyEditFormFromBot(detail);
|
||||||
|
setShowParamModal(true);
|
||||||
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SlidersHorizontal size={14} />
|
<SlidersHorizontal size={14} />
|
||||||
|
|
@ -3080,7 +3194,11 @@ export function BotDashboardModule({
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRuntimeMenuOpen(false);
|
setRuntimeMenuOpen(false);
|
||||||
setShowAgentModal(true);
|
void (async () => {
|
||||||
|
const detail = await ensureSelectedBotDetail();
|
||||||
|
applyEditFormFromBot(detail);
|
||||||
|
setShowAgentModal(true);
|
||||||
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FileText size={14} />
|
<FileText size={14} />
|
||||||
|
|
@ -3161,7 +3279,11 @@ export function BotDashboardModule({
|
||||||
<div className="section-mini-title">{t.workspaceOutputs}</div>
|
<div className="section-mini-title">{t.workspaceOutputs}</div>
|
||||||
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
||||||
<div className="workspace-toolbar">
|
<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">
|
<div className="workspace-toolbar-actions">
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="workspace-refresh-icon-btn"
|
className="workspace-refresh-icon-btn"
|
||||||
|
|
@ -3184,14 +3306,40 @@ export function BotDashboardModule({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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-panel">
|
||||||
<div className="workspace-list">
|
<div className="workspace-list">
|
||||||
{workspaceLoading ? (
|
{workspaceLoading || workspaceSearchLoading ? (
|
||||||
<div className="ops-empty-inline">{t.loadingDir}</div>
|
<div className="ops-empty-inline">{t.loadingDir}</div>
|
||||||
) : renderWorkspaceNodes(workspaceEntries).length === 0 ? (
|
) : renderWorkspaceNodes(filteredWorkspaceEntries).length === 0 ? (
|
||||||
<div className="ops-empty-inline">{t.emptyDir}</div>
|
<div className="ops-empty-inline">{normalizedWorkspaceQuery ? t.workspaceSearchNoResult : t.emptyDir}</div>
|
||||||
) : (
|
) : (
|
||||||
renderWorkspaceNodes(workspaceEntries)
|
renderWorkspaceNodes(filteredWorkspaceEntries)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="workspace-hint">
|
<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-label">{isZh ? '全称' : 'Name'}</span>
|
||||||
<span className="workspace-entry-info-value mono">{workspaceHoverCard.node.name || '-'}</span>
|
<span className="workspace-entry-info-value mono">{workspaceHoverCard.node.name || '-'}</span>
|
||||||
</div>
|
</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">
|
<div className="workspace-entry-info-row">
|
||||||
<span className="workspace-entry-info-label">{isZh ? '创建时间' : 'Created'}</span>
|
<span className="workspace-entry-info-label">{isZh ? '创建时间' : 'Created'}</span>
|
||||||
<span className="workspace-entry-info-value">{formatWorkspaceTime(workspaceHoverCard.node.ctime, isZh)}</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;
|
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) => ({
|
export const useAppStore = create<AppStore>((set) => ({
|
||||||
activeBots: {},
|
activeBots: {},
|
||||||
currentView: 'dashboard',
|
currentView: 'dashboard',
|
||||||
|
|
@ -61,6 +65,26 @@ export const useAppStore = create<AppStore>((set) => ({
|
||||||
nextBots[bot.id] = {
|
nextBots[bot.id] = {
|
||||||
...prev,
|
...prev,
|
||||||
...bot,
|
...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 ?? [],
|
logs: prev?.logs ?? [],
|
||||||
messages: prev?.messages ?? [],
|
messages: prev?.messages ?? [],
|
||||||
events: prev?.events ?? [],
|
events: prev?.events ?? [],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue