From d7a05651b7f9522accc5646cdf489d8eec619acd Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 2 Mar 2026 00:26:03 +0800 Subject: [PATCH] v0.1.1 --- .dockerignore | 17 + .env.prod.example | 31 + .gitignore | 62 + README.md | 121 + backend/.env.example | 26 + backend/.gitignore | 12 + backend/Dockerfile | 23 + backend/api/__init__.py | 0 backend/core/__init__.py | 0 backend/core/config_manager.py | 205 + backend/core/database.py | 80 + backend/core/docker_manager.py | 397 ++ backend/core/settings.py | 126 + backend/db/__init__.py | 0 backend/main.py | 1812 +++++++++ backend/models/__init__.py | 0 backend/models/bot.py | 50 + backend/requirements.txt | 16 + backend/schemas/__init__.py | 0 bot-images/Dashboard.Dockerfile | 31 + design/architecture.md | 64 + design/dashboard-nanobot.md | 131 + design/database.md | 130 + docker-compose.prod.yml | 75 + frontend/.env.example | 5 + frontend/.gitignore | 27 + frontend/Dockerfile | 29 + frontend/README.md | 73 + frontend/docker/nginx.conf | 37 + frontend/eslint.config.js | 23 + frontend/index.html | 15 + frontend/package.json | 44 + frontend/postcss.config.js | 6 + frontend/public/app-bot-icon.svg | 8 + frontend/public/nanobot-logo.png | Bin 0 -> 151546 bytes frontend/public/vite.svg | 1 + frontend/src/App.css | 1048 ++++++ frontend/src/App.tsx | 135 + frontend/src/assets/nanobot-logo.png | Bin 0 -> 151546 bytes frontend/src/assets/react.svg | 1 + frontend/src/components/BotManagementView.tsx | 1 + frontend/src/components/CreateBotModal.tsx | 1 + .../src/components/KernelManagerModal.tsx | 1 + frontend/src/components/VisualDeckView.tsx | 1 + frontend/src/components/VoxelBot.tsx | 1 + frontend/src/components/WorkingDeck.tsx | 1 + .../lucent/LucentPromptProvider.tsx | 168 + .../src/components/lucent/lucent-prompt.css | 146 + frontend/src/config/env.ts | 32 + frontend/src/hooks/useBotsSync.ts | 242 ++ frontend/src/i18n/README.md | 23 + frontend/src/i18n/app.en.ts | 15 + frontend/src/i18n/app.zh-cn.ts | 15 + frontend/src/i18n/bots-sync.en.ts | 5 + frontend/src/i18n/bots-sync.zh-cn.ts | 5 + frontend/src/i18n/channels.en.ts | 33 + frontend/src/i18n/channels.zh-cn.ts | 33 + frontend/src/i18n/dashboard.en.ts | 136 + frontend/src/i18n/dashboard.zh-cn.ts | 136 + frontend/src/i18n/image-factory.en.ts | 23 + frontend/src/i18n/image-factory.zh-cn.ts | 23 + frontend/src/i18n/index.ts | 11 + frontend/src/i18n/legacy-deck.en.ts | 11 + frontend/src/i18n/legacy-deck.zh-cn.ts | 11 + frontend/src/i18n/management.en.ts | 54 + frontend/src/i18n/management.zh-cn.ts | 54 + frontend/src/i18n/wizard.en.ts | 58 + frontend/src/i18n/wizard.zh-cn.ts | 58 + frontend/src/index.css | 7 + frontend/src/main.tsx | 13 + .../modules/dashboard/BotDashboardModule.css | 1573 ++++++++ .../modules/dashboard/BotDashboardModule.tsx | 2281 ++++++++++++ .../src/modules/dashboard/DashboardModule.tsx | 79 + .../modules/dashboard/components/VoxelBot.tsx | 51 + .../dashboard/components/WorkingDeck.tsx | 53 + .../src/modules/dashboard/messageParser.ts | 113 + .../src/modules/images/ImageFactoryModule.tsx | 257 ++ .../modules/management/ManagementModule.tsx | 171 + .../management/components/CreateBotModal.tsx | 189 + .../components/KernelManagerModal.tsx | 111 + .../modules/onboarding/BotWizardModule.tsx | 852 +++++ frontend/src/store/appStore.ts | 177 + frontend/src/types/bot.ts | 42 + frontend/tailwind.config.js | 21 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 7 + frontend/yarn.lock | 3287 +++++++++++++++++ scripts/deploy-prod.sh | 19 + scripts/stop-prod.sh | 7 + 91 files changed, 15530 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.prod.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/api/__init__.py create mode 100644 backend/core/__init__.py create mode 100644 backend/core/config_manager.py create mode 100644 backend/core/database.py create mode 100644 backend/core/docker_manager.py create mode 100644 backend/core/settings.py create mode 100644 backend/db/__init__.py create mode 100644 backend/main.py create mode 100644 backend/models/__init__.py create mode 100644 backend/models/bot.py create mode 100644 backend/requirements.txt create mode 100644 backend/schemas/__init__.py create mode 100644 bot-images/Dashboard.Dockerfile create mode 100644 design/architecture.md create mode 100644 design/dashboard-nanobot.md create mode 100644 design/database.md create mode 100644 docker-compose.prod.yml create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/docker/nginx.conf create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/app-bot-icon.svg create mode 100644 frontend/public/nanobot-logo.png create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/nanobot-logo.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/BotManagementView.tsx create mode 100644 frontend/src/components/CreateBotModal.tsx create mode 100644 frontend/src/components/KernelManagerModal.tsx create mode 100644 frontend/src/components/VisualDeckView.tsx create mode 100644 frontend/src/components/VoxelBot.tsx create mode 100644 frontend/src/components/WorkingDeck.tsx create mode 100644 frontend/src/components/lucent/LucentPromptProvider.tsx create mode 100644 frontend/src/components/lucent/lucent-prompt.css create mode 100644 frontend/src/config/env.ts create mode 100644 frontend/src/hooks/useBotsSync.ts create mode 100644 frontend/src/i18n/README.md create mode 100644 frontend/src/i18n/app.en.ts create mode 100644 frontend/src/i18n/app.zh-cn.ts create mode 100644 frontend/src/i18n/bots-sync.en.ts create mode 100644 frontend/src/i18n/bots-sync.zh-cn.ts create mode 100644 frontend/src/i18n/channels.en.ts create mode 100644 frontend/src/i18n/channels.zh-cn.ts create mode 100644 frontend/src/i18n/dashboard.en.ts create mode 100644 frontend/src/i18n/dashboard.zh-cn.ts create mode 100644 frontend/src/i18n/image-factory.en.ts create mode 100644 frontend/src/i18n/image-factory.zh-cn.ts create mode 100644 frontend/src/i18n/index.ts create mode 100644 frontend/src/i18n/legacy-deck.en.ts create mode 100644 frontend/src/i18n/legacy-deck.zh-cn.ts create mode 100644 frontend/src/i18n/management.en.ts create mode 100644 frontend/src/i18n/management.zh-cn.ts create mode 100644 frontend/src/i18n/wizard.en.ts create mode 100644 frontend/src/i18n/wizard.zh-cn.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/modules/dashboard/BotDashboardModule.css create mode 100644 frontend/src/modules/dashboard/BotDashboardModule.tsx create mode 100644 frontend/src/modules/dashboard/DashboardModule.tsx create mode 100644 frontend/src/modules/dashboard/components/VoxelBot.tsx create mode 100644 frontend/src/modules/dashboard/components/WorkingDeck.tsx create mode 100644 frontend/src/modules/dashboard/messageParser.ts create mode 100644 frontend/src/modules/images/ImageFactoryModule.tsx create mode 100644 frontend/src/modules/management/ManagementModule.tsx create mode 100644 frontend/src/modules/management/components/CreateBotModal.tsx create mode 100644 frontend/src/modules/management/components/KernelManagerModal.tsx create mode 100644 frontend/src/modules/onboarding/BotWizardModule.tsx create mode 100644 frontend/src/store/appStore.ts create mode 100644 frontend/src/types/bot.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/yarn.lock create mode 100755 scripts/deploy-prod.sh create mode 100755 scripts/stop-prod.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a2e82a0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.gitignore +**/.DS_Store + +frontend/node_modules +frontend/dist +backend/venv + +data +workspace + +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd + +*.log diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..404d39d --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,31 @@ +# Public exposed port (only nginx is exposed) +NGINX_PORT=8080 + +# REQUIRED absolute host paths. +# They must exist and be writable by docker daemon. +HOST_DATA_ROOT=/opt/dashboard-nanobot/data +HOST_BOTS_WORKSPACE_ROOT=/opt/dashboard-nanobot/workspace/bots + +# Optional custom image tags +BACKEND_IMAGE_TAG=latest +FRONTEND_IMAGE_TAG=latest + +# Optional build acceleration for China network +# If you mirror upstream container images, replace these with your mirror registry image names. +PYTHON_BASE_IMAGE=python:3.12-slim +NODE_BASE_IMAGE=node:20-alpine +NGINX_BASE_IMAGE=nginx:alpine + +# Python package index mirror (recommended in CN) +PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn + +# NPM registry mirror (recommended in CN) +NPM_REGISTRY=https://registry.npmmirror.com + +# Optional DB override. +# Keep empty to use SQLite at: +# sqlite:///{HOST_DATA_ROOT}/nanobot_dashboard.db +# DATABASE_URL=postgresql+psycopg://user:password@127.0.0.1:5432/nanobot_dashboard +# DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/nanobot_dashboard +DATABASE_URL= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5ed1d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# OS / Editor +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +.coverage.* +htmlcov/ + +# Virtual envs +.venv/ +venv/ +env/ +backend/venv/ + +# Backend runtime data +backend/__pycache__/ +backend/*.log + +# Project runtime data (generated locally) +data/ +workspace/ +engines/ + +# Frontend (Vite/Node) +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/.cache/ +frontend/coverage/ + +# Build artifacts +*.out +*.pid + +# Env / secrets +.env +.env.* +!.env.example +!.env.prod.example +backend/.env +frontend/.env +frontend/.env.* + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..75b7d5d --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Dashboard Nanobot + +Dashboard Nanobot 是面向 `nanobot` 的控制平面项目,提供镜像管理、引导创建和运行运维能力。 + +## 当前实现特性 + +- 零侵入引擎接入:通过 Docker 容器 + workspace 文件管理,不改 nanobot 源码。 +- 镜像登记流:只从 `docker images` 获取本地 `nanobot-base:*`,手工登记后才可用于创建。 +- 引导式创建:支持 provider/model 配置、模型连通性测试、标准模型参数配置。 +- Bootstrap 文件配置:支持创建时配置并持久化: + - `AGENTS.md` + - `SOUL.md` + - `USER.md` + - `TOOLS.md` + - `IDENTITY.md` +- 2D 运维 Dashboard:Bot 列表、启停、命令发送、日志流、遥测。 +- UI 全局支持:Light/Dark 切换、中文/English 切换。 + +## 架构概览 + +```mermaid +graph TD + User((User)) --> Frontend[Frontend Control Plane] + Frontend --> API[FastAPI Backend] + API --> DB[(SQLite)] + API --> Docker[Docker Daemon] + + Docker --> BotA[Bot Container A] + Docker --> BotB[Bot Container B] + + BotA --> WS[WebSocket State Stream] + BotB --> WS + WS --> Frontend +``` + +## 目录 + +```text +/dashboard-nanobot +├── backend/ +│ ├── core/ +│ ├── models/ +│ └── main.py +├── frontend/ +│ └── src/ +├── design/ +│ ├── dashboard-nanobot.md +│ ├── architecture.md +│ └── database.md +└── data/ +``` + +## 文档 + +- 技术规范书:`design/dashboard-nanobot.md` +- 架构设计:`design/architecture.md` +- 数据库设计:`design/database.md` + +## 环境变量配置 + +- 后端: + - 示例文件:`backend/.env.example` + - 本地配置:`backend/.env` + - 关键项: + - `DATABASE_URL`:数据库连接串(支持 SQLite / PostgreSQL / MySQL) + - `DATABASE_ECHO`:SQL 日志输出开关 + - `DATA_ROOT`、`BOTS_WORKSPACE_ROOT`:运行数据与 Bot 工作目录 + - `DEFAULT_*_MD`:创建向导默认模板来源(其中默认输出规范已并入 `DEFAULT_AGENTS_MD`) +- 前端: + - 示例文件:`frontend/.env.example` + - 本地配置:`frontend/.env` + - 关键项: + - `VITE_API_BASE`:后端 API 地址 + - `VITE_WS_BASE`:后端 WS 地址 + +创建向导中的 `soul_md / agents_md / user_md / tools_md / identity_md` +已改为后端统一下发(`GET /api/system/defaults`),前端不再写死。 + +## Docker 生产部署(推荐) + +### 目标 + +- 前后端均容器化部署 +- 后端 Python 3.12 +- 前端由 Nginx 托管静态资源并反代后端 API/WS +- 仅暴露 Nginx 端口(后端不对外暴露) +- 后端容器可通过 Docker Socket 启停 Bot 容器 + +### 文件 + +- `docker-compose.prod.yml` +- `backend/Dockerfile`(Python 3.12) +- `frontend/Dockerfile`(Nginx Web Server) +- `frontend/docker/nginx.conf` +- `.env.prod.example` +- `scripts/deploy-prod.sh` +- `scripts/stop-prod.sh` + +### 启动步骤 + +1. 准备部署变量 + - 复制 `.env.prod.example` 为 `.env.prod`(位于项目根目录) + - 配置绝对路径: + - `HOST_DATA_ROOT` + - `HOST_BOTS_WORKSPACE_ROOT` + - 中国网络建议配置加速项: + - `PIP_INDEX_URL`、`PIP_TRUSTED_HOST` + - `NPM_REGISTRY` + - 如需基础镜像加速,覆盖 `PYTHON_BASE_IMAGE` / `NODE_BASE_IMAGE` / `NGINX_BASE_IMAGE` +2. 启动服务 + - `./scripts/deploy-prod.sh` + - 或:`docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build` +3. 访问 + - `http://:${NGINX_PORT}`(默认 `8080`) + +### 关键说明 + +- `backend` 不开放宿主机端口,仅在内部网络被 Nginx 访问。 +- 必须挂载 `/var/run/docker.sock`,否则后端无法操作 Bot 镜像与容器。 +- `HOST_BOTS_WORKSPACE_ROOT` 必须是宿主机绝对路径,并且在 `docker-compose.prod.yml` 中以“同路径”挂载到后端容器。 + 原因:后端通过 Docker API 创建 Bot 容器时,使用的是宿主机可见的 bind 路径。 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..22de751 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,26 @@ +# Runtime paths +DATA_ROOT=../data +BOTS_WORKSPACE_ROOT=../workspace/bots + +# Database +# SQLite (recommended): leave DATABASE_URL unset, backend will use: +# sqlite:///{DATA_ROOT}/nanobot_dashboard.db +# DATABASE_URL=sqlite:///../data/nanobot_dashboard.db +# PostgreSQL example: +# DATABASE_URL=postgresql+psycopg://user:password@127.0.0.1:5432/nanobot_dashboard +# MySQL example: +# DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/nanobot_dashboard +# Show SQL statements in backend logs (debug only). +DATABASE_ECHO=true + +# Local backend server options (for `python3 main.py`) +APP_HOST=0.0.0.0 +APP_PORT=8000 +APP_RELOAD=true + +# Wizard / bootstrap default templates (use \n for line breaks) +DEFAULT_AGENTS_MD=# Agent Instructions\n\n- 优先完成任务目标\n- 操作前先说明意图\n- 输出必须可执行\n\n## 默认输出规范\n\n- 在workspace中创建目录保存输出。\n- 默认采用 Markdown(.md)格式。 +DEFAULT_SOUL_MD=# Soul\n\n你是专业的企业数字员工,表达清晰、可执行。 +DEFAULT_USER_MD=# User\n\n- 语言: 中文\n- 风格: 专业\n- 偏好: 简明且有步骤 +DEFAULT_TOOLS_MD=# Tools\n\n- 谨慎使用 shell\n- 修改文件后复核\n- 失败时说明原因并重试策略 +DEFAULT_IDENTITY_MD=# Identity\n\n- 角色: 企业数字员工\n- 领域: 运维与任务执行 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..64e852f --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*.log +.env +.env.* +!.env.example +.venv/ +venv/ +env/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..788e247 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +ARG PYTHON_BASE_IMAGE=python:3.12-slim +FROM ${PYTHON_BASE_IMAGE} + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app/backend + +ARG PIP_INDEX_URL=https://pypi.org/simple +ARG PIP_TRUSTED_HOST= + +COPY backend/requirements.txt ./requirements.txt +RUN if [ -n "${PIP_INDEX_URL}" ]; then pip config set global.index-url "${PIP_INDEX_URL}"; fi \ + && if [ -n "${PIP_TRUSTED_HOST}" ]; then pip config set global.trusted-host "${PIP_TRUSTED_HOST}"; fi \ + && pip install --upgrade pip \ + && pip install -r requirements.txt + +COPY backend/ /app/backend/ + +EXPOSE 8000 + +CMD ["python", "main.py"] diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/core/__init__.py b/backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/core/config_manager.py b/backend/core/config_manager.py new file mode 100644 index 0000000..12ecbd6 --- /dev/null +++ b/backend/core/config_manager.py @@ -0,0 +1,205 @@ +import json +import os +from typing import Any, Dict, List + +from core.settings import ( + DEFAULT_AGENTS_MD, + DEFAULT_IDENTITY_MD, + DEFAULT_SOUL_MD, + DEFAULT_TOOLS_MD, + DEFAULT_USER_MD, +) + + +class BotConfigManager: + def __init__(self, host_data_root: str): + self.host_data_root = host_data_root + + def update_workspace(self, bot_id: str, bot_data: Dict[str, Any], channels: List[Dict[str, Any]]): + """Generate/update nanobot workspace files and config.json.""" + bot_dir = os.path.join(self.host_data_root, bot_id) + dot_nanobot_dir = os.path.join(bot_dir, ".nanobot") + workspace_dir = os.path.join(dot_nanobot_dir, "workspace") + memory_dir = os.path.join(workspace_dir, "memory") + skills_dir = os.path.join(workspace_dir, "skills") + + for d in [dot_nanobot_dir, workspace_dir, memory_dir, skills_dir]: + os.makedirs(d, exist_ok=True) + + provider_name = (bot_data.get("llm_provider") or "openrouter").strip().lower() + model_name = (bot_data.get("llm_model") or "openai/gpt-4o-mini").strip() + api_key = (bot_data.get("api_key") or "").strip() + api_base = (bot_data.get("api_base") or "").strip() or None + + provider_alias = { + "aliyun": "dashscope", + "qwen": "dashscope", + "aliyun-qwen": "dashscope", + "moonshot": "kimi", + } + provider_name = provider_alias.get(provider_name, provider_name) + + provider_cfg: Dict[str, Any] = { + "apiKey": api_key, + } + if api_base: + provider_cfg["apiBase"] = api_base + + channels_cfg: Dict[str, Any] = { + "sendProgress": bool(bot_data.get("send_progress", False)), + "sendToolHints": bool(bot_data.get("send_tool_hints", False)), + } + + tools_cfg: Dict[str, Any] = { + "web": { + "search": { + "apiKey": "", + "maxResults": 5, + } + } + } + + raw_tools_cfg = bot_data.get("tools_config_json") + parsed_tools_cfg: Dict[str, Any] = {} + if isinstance(raw_tools_cfg, str) and raw_tools_cfg.strip(): + try: + loaded = json.loads(raw_tools_cfg) + if isinstance(loaded, dict): + parsed_tools_cfg = loaded + except Exception: + parsed_tools_cfg = {} + elif isinstance(raw_tools_cfg, dict): + parsed_tools_cfg = raw_tools_cfg + + web_cfg = parsed_tools_cfg.get("web") if isinstance(parsed_tools_cfg, dict) else None + if isinstance(web_cfg, dict): + search_cfg = web_cfg.get("search") + if isinstance(search_cfg, dict): + api_key = str(search_cfg.get("apiKey") or search_cfg.get("api_key") or "").strip() + max_results_raw = search_cfg.get("maxResults", search_cfg.get("max_results", 5)) + try: + max_results = int(max_results_raw) + except Exception: + max_results = 5 + max_results = max(1, min(max_results, 10)) + tools_cfg["web"]["search"]["apiKey"] = api_key + tools_cfg["web"]["search"]["maxResults"] = max_results + + config_data: Dict[str, Any] = { + "agents": { + "defaults": { + "model": model_name, + "temperature": float(bot_data.get("temperature") or 0.2), + "topP": float(bot_data.get("top_p") or 1.0), + "maxTokens": int(bot_data.get("max_tokens") or 8192), + } + }, + "providers": { + provider_name: provider_cfg, + }, + "channels": channels_cfg, + "tools": tools_cfg, + } + + for channel in channels: + channel_type = (channel.get("channel_type") or "").strip() + if not channel_type: + continue + raw_extra = channel.get("extra_config") + extra: Dict[str, Any] = {} + if isinstance(raw_extra, str) and raw_extra.strip(): + try: + parsed = json.loads(raw_extra) + if isinstance(parsed, dict): + extra = parsed + except Exception: + extra = {} + elif isinstance(raw_extra, dict): + extra = raw_extra + + # Dashboard channel is deprecated in DB routing. Global flags now come from bot fields. + if channel_type == "dashboard": + continue + + enabled = bool(channel.get("is_active", True)) + external = channel.get("external_app_id", "") or "" + secret = channel.get("app_secret", "") or "" + + if channel_type == "telegram": + channels_cfg["telegram"] = { + "enabled": enabled, + "token": secret, + "proxy": extra.get("proxy", ""), + "replyToMessage": bool(extra.get("replyToMessage", False)), + "allowFrom": extra.get("allowFrom", []), + } + continue + + if channel_type == "feishu": + channels_cfg["feishu"] = { + "enabled": enabled, + "appId": external, + "appSecret": secret, + "encryptKey": extra.get("encryptKey", ""), + "verificationToken": extra.get("verificationToken", ""), + "allowFrom": extra.get("allowFrom", []), + } + continue + + if channel_type == "dingtalk": + channels_cfg["dingtalk"] = { + "enabled": enabled, + "clientId": external, + "clientSecret": secret, + "allowFrom": extra.get("allowFrom", []), + } + continue + + if channel_type == "slack": + channels_cfg["slack"] = { + "enabled": enabled, + "mode": extra.get("mode", "socket"), + "botToken": external, + "appToken": secret, + "replyInThread": bool(extra.get("replyInThread", True)), + "groupPolicy": extra.get("groupPolicy", "mention"), + "groupAllowFrom": extra.get("groupAllowFrom", []), + "reactEmoji": extra.get("reactEmoji", "eyes"), + } + continue + + if channel_type == "qq": + channels_cfg["qq"] = { + "enabled": enabled, + "appId": external, + "secret": secret, + "allowFrom": extra.get("allowFrom", []), + } + continue + + # Fallback for future custom channels. + channels_cfg[channel_type] = { + "enabled": enabled, + "appId": external, + "appSecret": secret, + **extra, + } + + config_path = os.path.join(dot_nanobot_dir, "config.json") + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config_data, f, indent=4, ensure_ascii=False) + + bootstrap_files = { + "AGENTS.md": bot_data.get("agents_md") or DEFAULT_AGENTS_MD, + "SOUL.md": bot_data.get("soul_md") or bot_data.get("system_prompt") or DEFAULT_SOUL_MD, + "USER.md": bot_data.get("user_md") or DEFAULT_USER_MD, + "TOOLS.md": bot_data.get("tools_md") or DEFAULT_TOOLS_MD, + "IDENTITY.md": bot_data.get("identity_md") or DEFAULT_IDENTITY_MD, + } + + for filename, content in bootstrap_files.items(): + file_path = os.path.join(workspace_dir, filename) + with open(file_path, "w", encoding="utf-8") as f: + f.write(str(content).strip() + "\n") + + return dot_nanobot_dir diff --git a/backend/core/database.py b/backend/core/database.py new file mode 100644 index 0000000..cac6880 --- /dev/null +++ b/backend/core/database.py @@ -0,0 +1,80 @@ +from sqlalchemy import text +from sqlmodel import SQLModel, Session, create_engine + +from core.settings import DATABASE_ECHO, DATABASE_URL + +# Ensure table models are registered in SQLModel metadata before create_all. +from models import bot as _bot_models # noqa: F401 + +engine = create_engine(DATABASE_URL, echo=DATABASE_ECHO) + + +def _ensure_botinstance_columns() -> None: + required_columns = { + "current_state": "TEXT DEFAULT 'IDLE'", + "last_action": "TEXT", + "soul_md": "TEXT", + "agents_md": "TEXT", + "user_md": "TEXT", + "tools_md": "TEXT", + "tools_config_json": "TEXT", + "identity_md": "TEXT", + "api_base": "TEXT", + "temperature": "REAL DEFAULT 0.2", + "top_p": "REAL DEFAULT 1.0", + "max_tokens": "INTEGER DEFAULT 8192", + "presence_penalty": "REAL DEFAULT 0.0", + "frequency_penalty": "REAL DEFAULT 0.0", + "send_progress": "BOOLEAN DEFAULT 0", + "send_tool_hints": "BOOLEAN DEFAULT 0", + } + with engine.connect() as conn: + existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall() + existing = {str(row[1]) for row in existing_rows} + for col, ddl in required_columns.items(): + if col in existing: + continue + conn.execute(text(f"ALTER TABLE botinstance ADD COLUMN {col} {ddl}")) + conn.commit() + + +def _ensure_botmessage_columns() -> None: + required_columns = { + "media_json": "TEXT", + } + with engine.connect() as conn: + existing_rows = conn.execute(text("PRAGMA table_info(botmessage)")).fetchall() + existing = {str(row[1]) for row in existing_rows} + for col, ddl in required_columns.items(): + if col in existing: + continue + conn.execute(text(f"ALTER TABLE botmessage ADD COLUMN {col} {ddl}")) + conn.commit() + + +def _drop_legacy_skill_tables() -> None: + """Drop deprecated skill registry tables (moved to workspace filesystem mode).""" + with engine.connect() as conn: + conn.execute(text("DROP TABLE IF EXISTS botskillmapping")) + conn.execute(text("DROP TABLE IF EXISTS skillregistry")) + conn.commit() + + +def _drop_legacy_channel_tables() -> None: + """Drop deprecated channel route table (channel config source moved to config.json).""" + with engine.connect() as conn: + conn.execute(text("DROP TABLE IF EXISTS channelroute")) + conn.commit() + + +def init_database() -> None: + SQLModel.metadata.create_all(engine) + _drop_legacy_skill_tables() + _drop_legacy_channel_tables() + _ensure_botinstance_columns() + _ensure_botmessage_columns() + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/backend/core/docker_manager.py b/backend/core/docker_manager.py new file mode 100644 index 0000000..c3e8419 --- /dev/null +++ b/backend/core/docker_manager.py @@ -0,0 +1,397 @@ +import os +import re +import threading +import time +import codecs +from typing import Callable, Dict, List, Optional +import json + +import docker + + +class BotDockerManager: + def __init__(self, host_data_root: str, base_image: str = "nanobot-base:v0.1.4"): + try: + self.client = docker.from_env(timeout=6) + self.client.version() + print("✅ Docker 引擎连接成功") + except Exception as e: + self.client = None + print(f"⚠️ 警告: 无法连接到 Docker 引擎。请确保 Docker Desktop 已启动。错误: {e}") + + self.host_data_root = host_data_root + self.base_image = base_image + self.active_monitors = {} + + def has_image(self, tag: str) -> bool: + if not self.client: + return False + try: + self.client.images.get(tag) + return True + except docker.errors.ImageNotFound: + return False + except Exception: + return False + + def list_images_by_repo(self, repository: str = "nanobot-base") -> List[Dict[str, str]]: + """List docker images by repository prefix, returning normalized tag/id pairs.""" + if not self.client: + return [] + rows: List[Dict[str, str]] = [] + try: + images = self.client.images.list(name=repository) + for image in images: + for tag in image.tags: + repo, _, version = tag.partition(":") + if repo != repository or not version: + continue + rows.append( + { + "tag": tag, + "version": version.removeprefix("v"), + "image_id": image.id, + } + ) + except Exception as e: + print(f"[DockerManager] list_images_by_repo failed: {e}") + return rows + + def start_bot( + self, + bot_id: str, + image_tag: Optional[str] = None, + env_vars: Optional[Dict[str, str]] = None, + on_state_change: Optional[Callable[[str, dict], None]] = None, + ) -> bool: + if not self.client: + print("❌ 错误: Docker 客户端未初始化,无法启动机器人。") + return False + + image = image_tag or self.base_image + if not self.has_image(image): + print(f"❌ 错误: 镜像不存在: {image}") + return False + + bot_workspace = os.path.join(self.host_data_root, bot_id, ".nanobot") + container_name = f"worker_{bot_id}" + os.makedirs(bot_workspace, exist_ok=True) + + try: + try: + container = self.client.containers.get(container_name) + container.reload() + if container.status == "running": + if on_state_change: + self.ensure_monitor(bot_id, on_state_change) + return True + container.remove(force=True) + except docker.errors.NotFound: + pass + + container = self.client.containers.run( + image=image, + name=container_name, + detach=True, + stdin_open=True, + tty=True, + environment=env_vars or {}, + volumes={ + bot_workspace: {"bind": "/root/.nanobot", "mode": "rw"}, + }, + mem_limit="1g", + cpu_quota=100000, + network_mode="bridge", + ) + + if on_state_change: + monitor_thread = threading.Thread( + target=self._monitor_container_logs, + args=(bot_id, container, on_state_change), + daemon=True, + ) + monitor_thread.start() + self.active_monitors[bot_id] = monitor_thread + + return True + except Exception as e: + print(f"[DockerManager] Error starting bot {bot_id}: {e}") + return False + + def ensure_monitor(self, bot_id: str, on_state_change: Callable[[str, dict], None]) -> bool: + """Ensure an active log monitor exists for a running bot container.""" + if not self.client: + return False + existing = self.active_monitors.get(bot_id) + if existing and existing.is_alive(): + return True + try: + container = self.client.containers.get(f"worker_{bot_id}") + container.reload() + if container.status != "running": + return False + monitor_thread = threading.Thread( + target=self._monitor_container_logs, + args=(bot_id, container, on_state_change), + daemon=True, + ) + monitor_thread.start() + self.active_monitors[bot_id] = monitor_thread + return True + except docker.errors.NotFound: + return False + except Exception as e: + print(f"[DockerManager] Error ensuring monitor for {bot_id}: {e}") + return False + + def stop_bot(self, bot_id: str) -> bool: + if not self.client: + return False + container_name = f"worker_{bot_id}" + try: + container = self.client.containers.get(container_name) + container.stop(timeout=5) + container.remove() + self.active_monitors.pop(bot_id, None) + return True + except docker.errors.NotFound: + self.active_monitors.pop(bot_id, None) + return False + except Exception as e: + print(f"[DockerManager] Error stopping bot {bot_id}: {e}") + return False + + def send_command(self, bot_id: str, command: str, media: Optional[List[str]] = None) -> bool: + """Send a command to dashboard channel with robust container-local delivery.""" + if not self.client: + return False + media_paths = [str(v).strip().replace("\\", "/") for v in (media or []) if str(v).strip()] + + # Primary path on Docker Desktop/Mac: execute curl inside container namespace. + for attempt in range(3): + if self._send_command_via_exec(bot_id, command, media_paths): + return True + time.sleep(0.25 * (attempt + 1)) + + # Secondary path for environments where host can reach container IP. + return self._send_command_via_host_http(bot_id, command, media_paths) + + def _send_command_via_exec(self, bot_id: str, command: str, media: Optional[List[str]] = None) -> bool: + try: + container = self.client.containers.get(f"worker_{bot_id}") + payload_json = json.dumps({"message": command, "media": media or []}, ensure_ascii=False) + cmd = ( + "curl -sS --fail --max-time 6 " + "-X POST -H 'Content-Type: application/json' " + "-d \"$DASHBOARD_PAYLOAD\" http://127.0.0.1:9000/chat" + ) + result = container.exec_run(["/bin/sh", "-c", cmd], environment={"DASHBOARD_PAYLOAD": payload_json}) + output = result.output.decode("utf-8", errors="ignore") if isinstance(result.output, (bytes, bytearray)) else str(result.output) + if result.exit_code != 0: + print(f"[DockerManager] exec curl failed for {bot_id}: exit={result.exit_code}, out={output[:300]}") + return False + if output.strip(): + try: + parsed = json.loads(output) + if str(parsed.get("status", "")).lower() != "ok": + print(f"[DockerManager] exec curl non-ok response for {bot_id}: {output[:300]}") + return False + except Exception: + # Non-JSON but zero exit still treated as success. + pass + return True + except Exception as e: + print(f"[DockerManager] exec curl exception for {bot_id}: {e}") + return False + + def _send_command_via_host_http(self, bot_id: str, command: str, media: Optional[List[str]] = None) -> bool: + try: + import httpx + + container_name = f"worker_{bot_id}" + payload = {"message": command, "media": media or []} + container = self.client.containers.get(container_name) + ip_address = container.attrs["NetworkSettings"]["IPAddress"] or "127.0.0.1" + target_url = f"http://{ip_address}:9000/chat" + + with httpx.Client(timeout=4.0) as client: + resp = client.post(target_url, json=payload) + if resp.status_code == 200: + return True + print(f"[DockerManager] host HTTP failed: {resp.status_code} - {resp.text[:300]}") + return False + except Exception as e: + print(f"[DockerManager] host HTTP exception: {e}") + return False + + def get_recent_logs(self, bot_id: str, tail: int = 300) -> List[str]: + if not self.client: + return [] + container_name = f"worker_{bot_id}" + try: + container = self.client.containers.get(container_name) + raw = container.logs(tail=max(1, int(tail))) + text = raw.decode("utf-8", errors="ignore") + return [line for line in text.splitlines() if line.strip()] + except Exception as e: + print(f"[DockerManager] Error reading logs for {bot_id}: {e}") + return [] + + def _monitor_container_logs(self, bot_id: str, container, callback: Callable[[str, dict], None]): + try: + buffer = "" + dashboard_capture: Optional[str] = None + decoder = codecs.getincrementaldecoder("utf-8")("replace") + # Only tail new logs from "now" to avoid replaying historical stdout + # (which would repopulate cleared chat messages from old dashboard packets). + since_ts = int(time.time()) + for chunk in container.logs(stream=True, follow=True, since=since_ts): + if isinstance(chunk, bytes): + text = decoder.decode(chunk) + else: + text = str(chunk) + if not text: + continue + buffer += text + + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + normalized = line.strip("\r").strip() + if not normalized: + continue + + if dashboard_capture is not None: + dashboard_capture = f"{dashboard_capture}\n{normalized}" + if "__DASHBOARD_DATA_END__" in dashboard_capture: + state_packet = self._parse_dashboard_packet(dashboard_capture) + if state_packet: + callback(bot_id, state_packet) + dashboard_capture = None + callback(bot_id, {"type": "RAW_LOG", "text": normalized}) + continue + + if "__DASHBOARD_DATA_START__" in normalized and "__DASHBOARD_DATA_END__" not in normalized: + dashboard_capture = normalized + callback(bot_id, {"type": "RAW_LOG", "text": normalized}) + continue + + state_packet = self._parse_log_line(normalized) + if state_packet: + callback(bot_id, state_packet) + + callback(bot_id, {"type": "RAW_LOG", "text": normalized}) + + rest = decoder.decode(b"", final=True) + if rest: + buffer += rest + + tail = buffer.strip() + if tail: + state_packet = self._parse_log_line(tail) + if state_packet: + callback(bot_id, state_packet) + callback(bot_id, {"type": "RAW_LOG", "text": tail}) + except Exception as e: + print(f"[DockerManager] Log stream closed for {bot_id}: {e}") + + def _parse_dashboard_packet(self, line: str): + if "__DASHBOARD_DATA_START__" not in line or "__DASHBOARD_DATA_END__" not in line: + return None + try: + raw_json = line.split("__DASHBOARD_DATA_START__", 1)[1].split("__DASHBOARD_DATA_END__", 1)[0].strip() + data = json.loads(raw_json) + event_type = str(data.get("type", "")).upper() + content = str(data.get("content") or data.get("text") or "").strip() + media = [str(v).strip().replace("\\", "/") for v in (data.get("media") or []) if str(v).strip()] + is_progress = bool(data.get("is_progress", False)) + is_tool = bool(data.get("is_tool", False)) + + if event_type == "AGENT_STATE": + payload = data.get("payload") or {} + state = str(payload.get("state") or data.get("state") or ("TOOL_CALL" if is_tool else "THINKING")) + action_msg = str(payload.get("action_msg") or payload.get("msg") or content) + return { + "type": "AGENT_STATE", + "channel": "dashboard", + "payload": {"state": state, "action_msg": action_msg}, + } + + if event_type == "ASSISTANT_MESSAGE": + if content or media: + return {"type": "ASSISTANT_MESSAGE", "channel": "dashboard", "text": content, "media": media} + return None + + if event_type == "BUS_EVENT" or is_progress: + return { + "type": "BUS_EVENT", + "channel": "dashboard", + "content": content, + "media": media, + "is_progress": is_progress, + "is_tool": is_tool, + } + + if content or media: + return { + "type": "ASSISTANT_MESSAGE", + "channel": "dashboard", + "text": content, + "media": media, + } + except Exception: + return None + return None + + def _parse_log_line(self, line: str): + # 1. 结构化数据解析(首选,直接从机器人总线获取) + if "__DASHBOARD_DATA_START__" in line: + packet = self._parse_dashboard_packet(line) + if packet: + return packet + + # 2. 解析全渠道运行态日志(用于右侧状态面板) + process_match = re.search(r"Processing message from ([\w\-]+):[^:]+:\s*(.+)$", line) + if process_match: + channel = process_match.group(1).strip().lower() + action_msg = process_match.group(2).strip() + return { + "type": "AGENT_STATE", + "channel": channel, + "payload": { + "state": "THINKING", + "action_msg": action_msg[:4000], + }, + } + + response_match = re.search(r"Response to ([\w\-]+):[^:]+:\s*(.+)$", line) + if response_match: + channel = response_match.group(1).strip().lower() + action_msg = response_match.group(2).strip() + return { + "type": "AGENT_STATE", + "channel": channel, + "payload": { + "state": "SUCCESS", + "action_msg": action_msg[:4000], + }, + } + + # 3. 备选方案:常规日志解析 + lower = line.lower() + tool_call_match = re.search(r"tool call:\s*(.+)$", line, re.IGNORECASE) + if tool_call_match: + return { + "type": "AGENT_STATE", + "payload": { + "state": "TOOL_CALL", + "action_msg": tool_call_match.group(1).strip()[:4000], + }, + } + + if "error" in lower or "traceback" in lower: + return { + "type": "AGENT_STATE", + "payload": {"state": "ERROR", "action_msg": "执行异常,请检查日志"}, + } + + return None diff --git a/backend/core/settings.py b/backend/core/settings.py new file mode 100644 index 0000000..a807900 --- /dev/null +++ b/backend/core/settings.py @@ -0,0 +1,126 @@ +import os +from pathlib import Path +from typing import Final +from urllib.parse import urlsplit, urlunsplit + +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_dotenv(BACKEND_ROOT / ".env", override=False) +load_dotenv(PROJECT_ROOT / ".env", override=False) + + +def _env_text(name: str, default: str) -> str: + raw = os.getenv(name) + if raw is None: + return default + return str(raw).replace("\\n", "\n") + + +def _env_bool(name: str, default: bool) -> bool: + raw = os.getenv(name) + if raw is None: + return default + return str(raw).strip().lower() in {"1", "true", "yes", "on"} + + +def _normalize_dir_path(path_value: str) -> str: + raw = str(path_value or "").strip() + if not raw: + return raw + p = Path(raw) + if p.is_absolute(): + return str(p) + return str((BACKEND_ROOT / p).resolve()) + + +DATA_ROOT: Final[str] = _normalize_dir_path(os.getenv("DATA_ROOT", str(PROJECT_ROOT / "data"))) +BOTS_WORKSPACE_ROOT: Final[str] = _normalize_dir_path( + os.getenv("BOTS_WORKSPACE_ROOT", str(PROJECT_ROOT / "workspace" / "bots")) +) + + +def _normalize_database_url(url: str) -> str: + raw = str(url or "").strip() + prefix = "sqlite:///" + if not raw.startswith(prefix): + return raw + path_part = raw[len(prefix) :] + if not path_part or path_part.startswith("/"): + return raw + abs_path = (BACKEND_ROOT / path_part).resolve() + return f"{prefix}{abs_path.as_posix()}" + + +def _database_engine(url: str) -> str: + raw = str(url or "").strip().lower() + if raw.startswith("sqlite"): + return "sqlite" + if raw.startswith("postgresql"): + return "postgresql" + if raw.startswith("mysql"): + return "mysql" + if "+" in raw: + return raw.split("+", 1)[0] + if "://" in raw: + return raw.split("://", 1)[0] + return "unknown" + + +def _mask_database_url(url: str) -> str: + raw = str(url or "").strip() + if not raw or raw.startswith("sqlite"): + return raw + try: + parsed = urlsplit(raw) + if parsed.password is None: + return raw + host = parsed.hostname or "" + if parsed.port: + host = f"{host}:{parsed.port}" + auth = parsed.username or "" + if auth: + auth = f"{auth}:***@{host}" + else: + auth = host + netloc = auth + return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)) + except Exception: + return raw + + +_db_env = str(os.getenv("DATABASE_URL") or "").strip() +DATABASE_URL: Final[str] = _normalize_database_url( + _db_env if _db_env else f"sqlite:///{Path(DATA_ROOT) / 'nanobot_dashboard.db'}" +) +DATABASE_ENGINE: Final[str] = _database_engine(DATABASE_URL) +DATABASE_URL_DISPLAY: Final[str] = _mask_database_url(DATABASE_URL) +DATABASE_ECHO: Final[bool] = _env_bool("DATABASE_ECHO", True) + +DEFAULT_AGENTS_MD: Final[str] = _env_text( + "DEFAULT_AGENTS_MD", + "# Agent Instructions\n\n- 优先完成任务目标\n- 操作前先说明意图\n- 输出必须可执行\n\n## 默认输出规范\n\n- 每次执行任务时,在 workspace 中创建新目录保存本次输出。\n- 输出内容默认采用 Markdown(.md)格式。", +).strip() + +DEFAULT_SOUL_MD: Final[str] = _env_text( + "DEFAULT_SOUL_MD", + "# Soul\n\n你是专业的企业数字员工,表达清晰、可执行。", +).strip() + +DEFAULT_USER_MD: Final[str] = _env_text( + "DEFAULT_USER_MD", + "# User\n\n- 语言: 中文\n- 风格: 专业\n- 偏好: 简明且有步骤", +).strip() + +DEFAULT_TOOLS_MD: Final[str] = _env_text( + "DEFAULT_TOOLS_MD", + "# Tools\n\n- 谨慎使用 shell\n- 修改文件后复核\n- 失败时说明原因并重试策略", +).strip() + +DEFAULT_IDENTITY_MD: Final[str] = _env_text( + "DEFAULT_IDENTITY_MD", + "# Identity\n\n- 角色: 企业数字员工\n- 领域: 运维与任务执行", +).strip() diff --git a/backend/db/__init__.py b/backend/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..265cdaf --- /dev/null +++ b/backend/main.py @@ -0,0 +1,1812 @@ +import asyncio +import io +import json +import mimetypes +import os +import re +import shutil +import tempfile +import zipfile +from datetime import datetime +from typing import Any, Dict, List, Optional + +import httpx +from pydantic import BaseModel +from fastapi import Depends, FastAPI, File, HTTPException, UploadFile, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse +from fastapi.middleware.cors import CORSMiddleware +from sqlmodel import Session, select + +from core.config_manager import BotConfigManager +from core.database import engine, get_session, init_database +from core.docker_manager import BotDockerManager +from core.settings import ( + BOTS_WORKSPACE_ROOT, + DATA_ROOT, + DATABASE_ECHO, + DATABASE_ENGINE, + DATABASE_URL_DISPLAY, + DEFAULT_AGENTS_MD, + DEFAULT_IDENTITY_MD, + DEFAULT_SOUL_MD, + DEFAULT_TOOLS_MD, + DEFAULT_USER_MD, + PROJECT_ROOT, +) +from models.bot import BotInstance, BotMessage, NanobotImage + +app = FastAPI(title="Dashboard Nanobot API") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +os.makedirs(BOTS_WORKSPACE_ROOT, exist_ok=True) +os.makedirs(DATA_ROOT, exist_ok=True) + +docker_manager = BotDockerManager(host_data_root=BOTS_WORKSPACE_ROOT) +config_manager = BotConfigManager(host_data_root=BOTS_WORKSPACE_ROOT) + + +class ChannelConfigRequest(BaseModel): + channel_type: str + external_app_id: Optional[str] = None + app_secret: Optional[str] = None + internal_port: Optional[int] = None + is_active: bool = True + extra_config: Optional[Dict[str, Any]] = None + + +class ChannelConfigUpdateRequest(BaseModel): + channel_type: Optional[str] = None + external_app_id: Optional[str] = None + app_secret: Optional[str] = None + internal_port: Optional[int] = None + is_active: Optional[bool] = None + extra_config: Optional[Dict[str, Any]] = None + + +class BotCreateRequest(BaseModel): + id: str + name: str + llm_provider: str + llm_model: str + api_key: str + image_tag: str + system_prompt: Optional[str] = None + api_base: Optional[str] = None + temperature: float = 0.2 + top_p: float = 1.0 + max_tokens: int = 8192 + soul_md: Optional[str] = None + agents_md: Optional[str] = None + user_md: Optional[str] = None + tools_md: Optional[str] = None + tools_config: Optional[Dict[str, Any]] = None + env_params: Optional[Dict[str, str]] = None + identity_md: Optional[str] = None + channels: Optional[List[ChannelConfigRequest]] = None + send_progress: Optional[bool] = None + send_tool_hints: Optional[bool] = None + + +class BotUpdateRequest(BaseModel): + name: Optional[str] = None + llm_provider: Optional[str] = None + llm_model: Optional[str] = None + api_key: Optional[str] = None + api_base: Optional[str] = None + image_tag: Optional[str] = None + system_prompt: Optional[str] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + max_tokens: Optional[int] = None + soul_md: Optional[str] = None + agents_md: Optional[str] = None + user_md: Optional[str] = None + tools_md: Optional[str] = None + tools_config: Optional[Dict[str, Any]] = None + env_params: Optional[Dict[str, str]] = None + identity_md: Optional[str] = None + send_progress: Optional[bool] = None + send_tool_hints: Optional[bool] = None + + +class BotToolsConfigUpdateRequest(BaseModel): + tools_config: Optional[Dict[str, Any]] = None + + +class BotEnvParamsUpdateRequest(BaseModel): + env_params: Optional[Dict[str, str]] = None + + +class CommandRequest(BaseModel): + command: Optional[str] = None + attachments: Optional[List[str]] = None + + +def _normalize_media_item(bot_id: str, value: Any) -> str: + raw = str(value or "").strip().replace("\\", "/") + if not raw: + return "" + if raw.startswith("/root/.nanobot/workspace/"): + return raw[len("/root/.nanobot/workspace/") :].lstrip("/") + root = _workspace_root(bot_id) + if os.path.isabs(raw): + try: + if os.path.commonpath([root, raw]) == root: + return os.path.relpath(raw, root).replace("\\", "/") + except Exception: + pass + return raw.lstrip("/") + + +def _normalize_media_list(raw: Any, bot_id: str) -> List[str]: + if not isinstance(raw, list): + return [] + rows: List[str] = [] + for v in raw: + s = _normalize_media_item(bot_id, v) + if s: + rows.append(s) + return rows + + +def _persist_runtime_packet(bot_id: str, packet: Dict[str, Any]): + packet_type = str(packet.get("type", "")).upper() + if packet_type not in {"AGENT_STATE", "ASSISTANT_MESSAGE", "USER_COMMAND", "BUS_EVENT"}: + return + source_channel = str(packet.get("channel") or "").strip().lower() + if source_channel != "dashboard": + return + with Session(engine) as session: + bot = session.get(BotInstance, bot_id) + if not bot: + return + if packet_type == "AGENT_STATE": + payload = packet.get("payload") or {} + state = str(payload.get("state") or "").strip() + action = str(payload.get("action_msg") or payload.get("msg") or "").strip() + if state: + bot.current_state = state + if action: + bot.last_action = action[:4000] + elif packet_type == "ASSISTANT_MESSAGE": + bot.current_state = "IDLE" + text_msg = str(packet.get("text") or "").strip() + media_list = _normalize_media_list(packet.get("media"), bot_id) + if text_msg or media_list: + if text_msg: + bot.last_action = " ".join(text_msg.split())[:4000] + session.add( + BotMessage( + bot_id=bot_id, + role="assistant", + text=text_msg, + media_json=json.dumps(media_list, ensure_ascii=False) if media_list else None, + ) + ) + elif packet_type == "USER_COMMAND": + text_msg = str(packet.get("text") or "").strip() + media_list = _normalize_media_list(packet.get("media"), bot_id) + if text_msg or media_list: + session.add( + BotMessage( + bot_id=bot_id, + role="user", + text=text_msg, + media_json=json.dumps(media_list, ensure_ascii=False) if media_list else None, + ) + ) + elif packet_type == "BUS_EVENT": + # Dashboard channel emits BUS_EVENT for both progress and final replies. + # Persist only non-progress events to keep durable chat history clean. + is_progress = bool(packet.get("is_progress")) + if not is_progress: + text_msg = str(packet.get("content") or packet.get("text") or "").strip() + media_list = _normalize_media_list(packet.get("media"), bot_id) + if text_msg or media_list: + bot.current_state = "IDLE" + if text_msg: + bot.last_action = " ".join(text_msg.split())[:4000] + session.add( + BotMessage( + bot_id=bot_id, + role="assistant", + text=text_msg, + media_json=json.dumps(media_list, ensure_ascii=False) if media_list else None, + ) + ) + + bot.updated_at = datetime.utcnow() + session.add(bot) + session.commit() + + +class WSConnectionManager: + def __init__(self): + self.connections: Dict[str, List[WebSocket]] = {} + + async def connect(self, bot_id: str, websocket: WebSocket): + await websocket.accept() + self.connections.setdefault(bot_id, []).append(websocket) + + def disconnect(self, bot_id: str, websocket: WebSocket): + conns = self.connections.get(bot_id, []) + if websocket in conns: + conns.remove(websocket) + if not conns and bot_id in self.connections: + del self.connections[bot_id] + + async def broadcast(self, bot_id: str, data: Dict[str, Any]): + conns = list(self.connections.get(bot_id, [])) + for ws in conns: + try: + await ws.send_json(data) + except Exception: + self.disconnect(bot_id, ws) + + +manager = WSConnectionManager() + + +def docker_callback(bot_id: str, packet: Dict[str, Any]): + _persist_runtime_packet(bot_id, packet) + loop = getattr(app.state, "main_loop", None) + if not loop or not loop.is_running(): + return + asyncio.run_coroutine_threadsafe(manager.broadcast(bot_id, packet), loop) + + +@app.on_event("startup") +async def on_startup(): + app.state.main_loop = asyncio.get_running_loop() + print(f"📁 项目根目录: {PROJECT_ROOT}") + print(f"🗄️ 数据库引擎: {DATABASE_ENGINE} (echo={DATABASE_ECHO})") + print(f"📁 数据库连接: {DATABASE_URL_DISPLAY}") + init_database() + with Session(engine) as session: + running_bots = session.exec(select(BotInstance).where(BotInstance.docker_status == "RUNNING")).all() + for bot in running_bots: + docker_manager.ensure_monitor(bot.id, docker_callback) + + +def _provider_defaults(provider: str) -> tuple[str, str]: + p = provider.lower().strip() + if p in {"openrouter"}: + return "openrouter", "https://openrouter.ai/api/v1" + if p in {"dashscope", "aliyun", "qwen", "aliyun-qwen"}: + return "dashscope", "https://dashscope.aliyuncs.com/compatible-mode/v1" + if p in {"kimi", "moonshot"}: + return "kimi", "https://api.moonshot.cn/v1" + if p in {"minimax"}: + return "minimax", "https://api.minimax.chat/v1" + return p, "" + + +@app.get("/api/system/defaults") +def get_system_defaults(): + return { + "templates": { + "soul_md": DEFAULT_SOUL_MD, + "agents_md": DEFAULT_AGENTS_MD, + "user_md": DEFAULT_USER_MD, + "tools_md": DEFAULT_TOOLS_MD, + "identity_md": DEFAULT_IDENTITY_MD, + } + } + + +@app.get("/api/health") +def get_health(): + try: + with Session(engine) as session: + session.exec(select(BotInstance).limit(1)).first() + return {"status": "ok", "database": DATABASE_ENGINE} + except Exception as e: + raise HTTPException(status_code=503, detail=f"database check failed: {e}") + + +def _config_json_path(bot_id: str) -> str: + return os.path.join(_bot_data_root(bot_id), "config.json") + + +def _read_bot_config(bot_id: str) -> Dict[str, Any]: + path = _config_json_path(bot_id) + if not os.path.isfile(path): + return {} + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _write_bot_config(bot_id: str, config_data: Dict[str, Any]) -> None: + path = _config_json_path(bot_id) + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = f"{path}.tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(config_data, f, ensure_ascii=False, indent=2) + os.replace(tmp, path) + + +def _normalize_channel_extra(raw: Any) -> Dict[str, Any]: + if not isinstance(raw, dict): + return {} + return raw + + +def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Dict[str, Any]: + ctype = str(ctype or "").strip().lower() + enabled = bool(cfg.get("enabled", True)) + port = max(1, min(int(cfg.get("port", 8080) or 8080), 65535)) + extra: Dict[str, Any] = {} + external_app_id = "" + app_secret = "" + + if ctype == "feishu": + external_app_id = str(cfg.get("appId") or "") + app_secret = str(cfg.get("appSecret") or "") + extra = { + "encryptKey": cfg.get("encryptKey", ""), + "verificationToken": cfg.get("verificationToken", ""), + "allowFrom": cfg.get("allowFrom", []), + } + elif ctype == "dingtalk": + external_app_id = str(cfg.get("clientId") or "") + app_secret = str(cfg.get("clientSecret") or "") + extra = {"allowFrom": cfg.get("allowFrom", [])} + elif ctype == "telegram": + app_secret = str(cfg.get("token") or "") + extra = { + "proxy": cfg.get("proxy", ""), + "replyToMessage": bool(cfg.get("replyToMessage", False)), + "allowFrom": cfg.get("allowFrom", []), + } + elif ctype == "slack": + external_app_id = str(cfg.get("botToken") or "") + app_secret = str(cfg.get("appToken") or "") + extra = { + "mode": cfg.get("mode", "socket"), + "replyInThread": bool(cfg.get("replyInThread", True)), + "groupPolicy": cfg.get("groupPolicy", "mention"), + "groupAllowFrom": cfg.get("groupAllowFrom", []), + "reactEmoji": cfg.get("reactEmoji", "eyes"), + } + elif ctype == "qq": + external_app_id = str(cfg.get("appId") or "") + app_secret = str(cfg.get("secret") or "") + extra = {"allowFrom": cfg.get("allowFrom", [])} + else: + external_app_id = str( + cfg.get("appId") or cfg.get("clientId") or cfg.get("botToken") or cfg.get("externalAppId") or "" + ) + app_secret = str( + cfg.get("appSecret") or cfg.get("clientSecret") or cfg.get("secret") or cfg.get("token") or cfg.get("appToken") or "" + ) + extra = {k: v for k, v in cfg.items() if k not in {"enabled", "port", "appId", "clientId", "botToken", "externalAppId", "appSecret", "clientSecret", "secret", "token", "appToken"}} + + return { + "id": ctype, + "bot_id": bot_id, + "channel_type": ctype, + "external_app_id": external_app_id, + "app_secret": app_secret, + "internal_port": port, + "is_active": enabled, + "extra_config": extra, + "locked": ctype == "dashboard", + } + + +def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]: + ctype = str(row.get("channel_type") or "").strip().lower() + enabled = bool(row.get("is_active", True)) + extra = _normalize_channel_extra(row.get("extra_config")) + external_app_id = str(row.get("external_app_id") or "") + app_secret = str(row.get("app_secret") or "") + port = max(1, min(int(row.get("internal_port") or 8080), 65535)) + + if ctype == "feishu": + return { + "enabled": enabled, + "appId": external_app_id, + "appSecret": app_secret, + "encryptKey": extra.get("encryptKey", ""), + "verificationToken": extra.get("verificationToken", ""), + "allowFrom": extra.get("allowFrom", []), + } + if ctype == "dingtalk": + return { + "enabled": enabled, + "clientId": external_app_id, + "clientSecret": app_secret, + "allowFrom": extra.get("allowFrom", []), + } + if ctype == "telegram": + return { + "enabled": enabled, + "token": app_secret, + "proxy": extra.get("proxy", ""), + "replyToMessage": bool(extra.get("replyToMessage", False)), + "allowFrom": extra.get("allowFrom", []), + } + if ctype == "slack": + return { + "enabled": enabled, + "mode": extra.get("mode", "socket"), + "botToken": external_app_id, + "appToken": app_secret, + "replyInThread": bool(extra.get("replyInThread", True)), + "groupPolicy": extra.get("groupPolicy", "mention"), + "groupAllowFrom": extra.get("groupAllowFrom", []), + "reactEmoji": extra.get("reactEmoji", "eyes"), + } + if ctype == "qq": + return { + "enabled": enabled, + "appId": external_app_id, + "secret": app_secret, + "allowFrom": extra.get("allowFrom", []), + } + merged = dict(extra) + merged.update( + { + "enabled": enabled, + "appId": external_app_id, + "appSecret": app_secret, + "port": port, + } + ) + return merged + + +def _get_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]: + config_data = _read_bot_config(bot.id) + channels_cfg = config_data.get("channels") + if not isinstance(channels_cfg, dict): + channels_cfg = {} + + send_progress = bool(channels_cfg.get("sendProgress", bot.send_progress)) + send_tool_hints = bool(channels_cfg.get("sendToolHints", bot.send_tool_hints)) + rows: List[Dict[str, Any]] = [ + { + "id": "dashboard", + "bot_id": bot.id, + "channel_type": "dashboard", + "external_app_id": f"dashboard-{bot.id}", + "app_secret": "", + "internal_port": 9000, + "is_active": True, + "extra_config": { + "sendProgress": send_progress, + "sendToolHints": send_tool_hints, + }, + "locked": True, + } + ] + + for ctype, cfg in channels_cfg.items(): + if ctype in {"sendProgress", "sendToolHints"}: + continue + if not isinstance(cfg, dict): + continue + rows.append(_channel_cfg_to_api_dict(bot.id, ctype, cfg)) + return rows + + +def _normalize_initial_channels(bot_id: str, channels: Optional[List[ChannelConfigRequest]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + seen_types: set[str] = set() + for c in channels or []: + ctype = (c.channel_type or "").strip().lower() + if not ctype or ctype == "dashboard" or ctype in seen_types: + continue + seen_types.add(ctype) + rows.append( + { + "id": ctype, + "bot_id": bot_id, + "channel_type": ctype, + "external_app_id": (c.external_app_id or "").strip() or f"{ctype}-{bot_id}", + "app_secret": (c.app_secret or "").strip(), + "internal_port": max(1, min(int(c.internal_port or 8080), 65535)), + "is_active": bool(c.is_active), + "extra_config": _normalize_channel_extra(c.extra_config), + "locked": False, + } + ) + return rows + + +def _parse_message_media(bot_id: str, media_raw: Optional[str]) -> List[str]: + if not media_raw: + return [] + try: + parsed = json.loads(media_raw) + return _normalize_media_list(parsed, bot_id) + except Exception: + return [] + + +def _default_tools_config() -> Dict[str, Any]: + return { + "web": { + "search": { + "apiKey": "", + "maxResults": 5, + } + } + } + + +def _normalize_tools_config(raw: Any) -> Dict[str, Any]: + cfg = _default_tools_config() + if not isinstance(raw, dict): + return cfg + + web_raw = raw.get("web") + if isinstance(web_raw, dict): + search_raw = web_raw.get("search") + if isinstance(search_raw, dict): + api_key = str(search_raw.get("apiKey") or search_raw.get("api_key") or "").strip() + max_results_raw = search_raw.get("maxResults", search_raw.get("max_results", 5)) + try: + max_results = int(max_results_raw) + except Exception: + max_results = 5 + max_results = max(1, min(max_results, 10)) + cfg["web"]["search"]["apiKey"] = api_key + cfg["web"]["search"]["maxResults"] = max_results + return cfg + + +def _parse_tools_config(raw: Optional[str]) -> Dict[str, Any]: + if not raw: + return _default_tools_config() + try: + parsed = json.loads(raw) + except Exception: + return _default_tools_config() + return _normalize_tools_config(parsed) + + +_ENV_KEY_RE = re.compile(r"^[A-Z_][A-Z0-9_]{0,127}$") + + +def _normalize_env_params(raw: Any) -> Dict[str, str]: + if not isinstance(raw, dict): + return {} + rows: Dict[str, str] = {} + for k, v in raw.items(): + key = str(k or "").strip().upper() + if not key or not _ENV_KEY_RE.fullmatch(key): + continue + rows[key] = str(v or "").strip() + return rows + + +def _parse_env_params(raw: Any) -> Dict[str, str]: + return _normalize_env_params(raw) + + +def _sync_workspace_channels( + session: Session, + bot_id: str, + channels_override: Optional[List[Dict[str, Any]]] = None, +) -> None: + bot = session.get(BotInstance, bot_id) + if not bot: + return + channels_data = channels_override if channels_override is not None else _get_bot_channels_from_config(bot) + bot_data = bot.model_dump() + for row in channels_data: + if str(row.get("channel_type") or "").strip().lower() != "dashboard": + continue + extra = _normalize_channel_extra(row.get("extra_config")) + bot_data["send_progress"] = bool(extra.get("sendProgress", bot.send_progress)) + bot_data["send_tool_hints"] = bool(extra.get("sendToolHints", bot.send_tool_hints)) + break + normalized_channels: List[Dict[str, Any]] = [] + for row in channels_data: + ctype = str(row.get("channel_type") or "").strip().lower() + if not ctype or ctype == "dashboard": + continue + normalized_channels.append( + { + "channel_type": ctype, + "external_app_id": str(row.get("external_app_id") or ""), + "app_secret": str(row.get("app_secret") or ""), + "internal_port": max(1, min(int(row.get("internal_port") or 8080), 65535)), + "is_active": bool(row.get("is_active", True)), + "extra_config": _normalize_channel_extra(row.get("extra_config")), + } + ) + config_manager.update_workspace( + bot_id=bot_id, + bot_data=bot_data, + channels=normalized_channels, + ) + + +def reconcile_image_registry(session: Session): + """Only reconcile status for images explicitly registered in DB.""" + db_images = session.exec(select(NanobotImage)).all() + for img in db_images: + if docker_manager.has_image(img.tag): + try: + docker_img = docker_manager.client.images.get(img.tag) if docker_manager.client else None + img.image_id = docker_img.id if docker_img else img.image_id + except Exception: + pass + img.status = "READY" + else: + img.status = "UNKNOWN" + session.add(img) + + session.commit() + + +def _workspace_root(bot_id: str) -> str: + return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot", "workspace")) + + +def _bot_data_root(bot_id: str) -> str: + return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot")) + + +def _skills_root(bot_id: str) -> str: + return os.path.join(_workspace_root(bot_id), "skills") + + +def _is_valid_top_level_skill_name(name: str) -> bool: + text = str(name or "").strip() + if not text: + return False + if "/" in text or "\\" in text: + return False + if text in {".", ".."}: + return False + return True + + +def _read_skill_description(entry_path: str) -> str: + candidates: List[str] = [] + if os.path.isdir(entry_path): + candidates = [ + os.path.join(entry_path, "SKILL.md"), + os.path.join(entry_path, "skill.md"), + os.path.join(entry_path, "README.md"), + os.path.join(entry_path, "readme.md"), + ] + elif entry_path.lower().endswith(".md"): + candidates = [entry_path] + + for candidate in candidates: + if not os.path.isfile(candidate): + continue + try: + with open(candidate, "r", encoding="utf-8") as f: + for line in f: + text = line.strip() + if text and not text.startswith("#"): + return text[:240] + except Exception: + continue + return "" + + +def _list_workspace_skills(bot_id: str) -> List[Dict[str, Any]]: + root = _skills_root(bot_id) + os.makedirs(root, exist_ok=True) + rows: List[Dict[str, Any]] = [] + names = sorted(os.listdir(root), key=lambda n: (not os.path.isdir(os.path.join(root, n)), n.lower())) + for name in names: + if not name or name.startswith("."): + continue + if not _is_valid_top_level_skill_name(name): + continue + abs_path = os.path.join(root, name) + if not os.path.exists(abs_path): + continue + stat = os.stat(abs_path) + rows.append( + { + "id": name, + "name": name, + "type": "dir" if os.path.isdir(abs_path) else "file", + "path": f"skills/{name}", + "size": stat.st_size if os.path.isfile(abs_path) else None, + "mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z", + "description": _read_skill_description(abs_path), + } + ) + return rows + + +def _cron_store_path(bot_id: str) -> str: + return os.path.join(_bot_data_root(bot_id), "cron", "jobs.json") + + +def _env_store_path(bot_id: str) -> str: + return os.path.join(_bot_data_root(bot_id), "env.json") + + +def _read_env_store(bot_id: str) -> Dict[str, str]: + path = _env_store_path(bot_id) + if not os.path.isfile(path): + return {} + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return _normalize_env_params(data) + except Exception: + return {} + + +def _write_env_store(bot_id: str, env_params: Dict[str, str]) -> None: + path = _env_store_path(bot_id) + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = f"{path}.tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(_normalize_env_params(env_params), f, ensure_ascii=False, indent=2) + os.replace(tmp, path) + + +def _read_cron_store(bot_id: str) -> Dict[str, Any]: + path = _cron_store_path(bot_id) + if not os.path.isfile(path): + return {"version": 1, "jobs": []} + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + return {"version": 1, "jobs": []} + jobs = data.get("jobs") + if not isinstance(jobs, list): + data["jobs"] = [] + if "version" not in data: + data["version"] = 1 + return data + except Exception: + return {"version": 1, "jobs": []} + + +def _write_cron_store(bot_id: str, store: Dict[str, Any]) -> None: + path = _cron_store_path(bot_id) + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = f"{path}.tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(store, f, ensure_ascii=False, indent=2) + os.replace(tmp, path) + + +def _resolve_workspace_path(bot_id: str, rel_path: Optional[str] = None) -> tuple[str, str]: + root = _workspace_root(bot_id) + rel = (rel_path or "").strip().replace("\\", "/") + target = os.path.abspath(os.path.join(root, rel)) + if os.path.commonpath([root, target]) != root: + raise HTTPException(status_code=400, detail="invalid workspace path") + return root, target + + +def _build_workspace_tree(path: str, root: str, depth: int) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + try: + names = sorted(os.listdir(path), key=lambda v: (not os.path.isdir(os.path.join(path, v)), v.lower())) + except FileNotFoundError: + return rows + + for name in names: + if name in {".DS_Store"}: + continue + abs_path = os.path.join(path, name) + rel_path = os.path.relpath(abs_path, root).replace("\\", "/") + stat = os.stat(abs_path) + base: Dict[str, Any] = { + "name": name, + "path": rel_path, + "mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z", + } + if os.path.isdir(abs_path): + node = {**base, "type": "dir"} + if depth > 0: + node["children"] = _build_workspace_tree(abs_path, root, depth - 1) + rows.append(node) + continue + rows.append( + { + **base, + "type": "file", + "size": stat.st_size, + "ext": os.path.splitext(name)[1].lower(), + } + ) + return rows + + +def _list_workspace_dir(path: str, root: str) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + names = sorted(os.listdir(path), key=lambda v: (not os.path.isdir(os.path.join(path, v)), v.lower())) + for name in names: + if name in {".DS_Store"}: + continue + abs_path = os.path.join(path, name) + rel_path = os.path.relpath(abs_path, root).replace("\\", "/") + stat = os.stat(abs_path) + rows.append( + { + "name": name, + "path": rel_path, + "type": "dir" if os.path.isdir(abs_path) else "file", + "size": stat.st_size if os.path.isfile(abs_path) else None, + "ext": os.path.splitext(name)[1].lower() if os.path.isfile(abs_path) else "", + "mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z", + } + ) + return rows + + +@app.get("/api/images", response_model=List[NanobotImage]) +def list_images(session: Session = Depends(get_session)): + reconcile_image_registry(session) + return session.exec(select(NanobotImage)).all() + + +@app.delete("/api/images/{tag:path}") +def delete_image(tag: str, session: Session = Depends(get_session)): + image = session.get(NanobotImage, tag) + if not image: + raise HTTPException(status_code=404, detail="Image not found") + + # 检查是否有机器人正在使用此镜像 + bots_using = session.exec(select(BotInstance).where(BotInstance.image_tag == tag)).all() + if bots_using: + raise HTTPException(status_code=400, detail=f"Cannot delete image: {len(bots_using)} bots are using it.") + + session.delete(image) + session.commit() + return {"status": "deleted"} + + +@app.get("/api/docker-images") +def list_docker_images(repository: str = "nanobot-base"): + rows = docker_manager.list_images_by_repo(repository) + return rows + + +@app.post("/api/images/register") +def register_image(payload: dict, session: Session = Depends(get_session)): + tag = (payload.get("tag") or "").strip() + source_dir = (payload.get("source_dir") or "manual").strip() or "manual" + if not tag: + raise HTTPException(status_code=400, detail="tag is required") + + if not docker_manager.has_image(tag): + raise HTTPException(status_code=404, detail=f"Docker image not found: {tag}") + + version = tag.split(":")[-1].removeprefix("v") if ":" in tag else tag + try: + docker_img = docker_manager.client.images.get(tag) if docker_manager.client else None + image_id = docker_img.id if docker_img else None + except Exception: + image_id = None + + row = session.get(NanobotImage, tag) + if not row: + row = NanobotImage( + tag=tag, + version=version, + status="READY", + source_dir=source_dir, + image_id=image_id, + ) + else: + row.version = version + row.status = "READY" + row.source_dir = source_dir + row.image_id = image_id + session.add(row) + session.commit() + session.refresh(row) + return row + + +@app.post("/api/providers/test") +async def test_provider(payload: dict): + provider = (payload.get("provider") or "").strip() + api_key = (payload.get("api_key") or "").strip() + model = (payload.get("model") or "").strip() + api_base = (payload.get("api_base") or "").strip() + + if not provider or not api_key: + raise HTTPException(status_code=400, detail="provider and api_key are required") + + normalized_provider, default_base = _provider_defaults(provider) + base = (api_base or default_base).rstrip("/") + + if normalized_provider not in {"openrouter", "dashscope", "kimi", "minimax"}: + raise HTTPException(status_code=400, detail=f"provider not supported for test: {provider}") + + if not base: + raise HTTPException(status_code=400, detail=f"api_base is required for provider: {provider}") + + headers = {"Authorization": f"Bearer {api_key}"} + timeout = httpx.Timeout(20.0, connect=10.0) + url = f"{base}/models" + + try: + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.get(url, headers=headers) + + if resp.status_code >= 400: + return { + "ok": False, + "provider": normalized_provider, + "status_code": resp.status_code, + "detail": resp.text[:500], + } + + data = resp.json() + models_raw = data.get("data", []) if isinstance(data, dict) else [] + model_ids: List[str] = [] + for item in models_raw[:20]: + if isinstance(item, dict) and item.get("id"): + model_ids.append(str(item["id"])) + + model_hint = "" + if model: + model_hint = "model_found" if any(model in m for m in model_ids) else "model_not_listed" + + return { + "ok": True, + "provider": normalized_provider, + "endpoint": url, + "models_preview": model_ids[:8], + "model_hint": model_hint, + } + except Exception as e: + return { + "ok": False, + "provider": normalized_provider, + "endpoint": url, + "detail": str(e), + } + + +@app.post("/api/bots", response_model=BotInstance) +def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)): + image_row = session.get(NanobotImage, payload.image_tag) + if not image_row: + raise HTTPException(status_code=400, detail=f"Image not registered in DB: {payload.image_tag}") + if image_row.status != "READY": + raise HTTPException(status_code=400, detail=f"Image status is not READY: {payload.image_tag} ({image_row.status})") + if not docker_manager.has_image(payload.image_tag): + raise HTTPException(status_code=400, detail=f"Docker image not found locally: {payload.image_tag}") + + bot = BotInstance( + id=payload.id, + name=payload.name, + system_prompt=payload.system_prompt or payload.soul_md or DEFAULT_SOUL_MD, + soul_md=payload.soul_md or DEFAULT_SOUL_MD, + agents_md=payload.agents_md or DEFAULT_AGENTS_MD, + user_md=payload.user_md or DEFAULT_USER_MD, + tools_md=payload.tools_md or DEFAULT_TOOLS_MD, + tools_config_json=json.dumps(_normalize_tools_config(payload.tools_config), ensure_ascii=False), + identity_md=payload.identity_md or DEFAULT_IDENTITY_MD, + llm_provider=payload.llm_provider, + llm_model=payload.llm_model, + api_key=payload.api_key, + api_base=payload.api_base, + temperature=payload.temperature, + top_p=payload.top_p, + max_tokens=payload.max_tokens, + send_progress=bool(payload.send_progress) if payload.send_progress is not None else False, + send_tool_hints=bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False, + image_tag=payload.image_tag, + workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id), + ) + + session.add(bot) + session.commit() + session.refresh(bot) + _write_env_store(payload.id, _normalize_env_params(payload.env_params)) + _sync_workspace_channels(session, payload.id, channels_override=_normalize_initial_channels(payload.id, payload.channels)) + return bot + + +@app.get("/api/bots", response_model=List[BotInstance]) +def list_bots(session: Session = Depends(get_session)): + return session.exec(select(BotInstance)).all() + + +@app.put("/api/bots/{bot_id}", response_model=BotInstance) +def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + update_data = payload.model_dump(exclude_unset=True) + + if "image_tag" in update_data and update_data["image_tag"]: + image_tag = str(update_data["image_tag"]).strip() + image_row = session.get(NanobotImage, image_tag) + if not image_row: + raise HTTPException(status_code=400, detail=f"Image not registered in DB: {image_tag}") + if image_row.status != "READY": + raise HTTPException(status_code=400, detail=f"Image status is not READY: {image_tag} ({image_row.status})") + if not docker_manager.has_image(image_tag): + raise HTTPException(status_code=400, detail=f"Docker image not found locally: {image_tag}") + + tools_config = update_data.pop("tools_config", None) if isinstance(update_data, dict) else None + if tools_config is not None: + bot.tools_config_json = json.dumps(_normalize_tools_config(tools_config), ensure_ascii=False) + env_params = update_data.pop("env_params", None) if isinstance(update_data, dict) else None + + for key, value in update_data.items(): + setattr(bot, key, value) + + session.add(bot) + session.commit() + session.refresh(bot) + if env_params is not None: + _write_env_store(bot_id, _normalize_env_params(env_params)) + _sync_workspace_channels(session, bot_id) + return bot + + +@app.post("/api/bots/{bot_id}/start") +async def start_bot(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + _sync_workspace_channels(session, bot_id) + + env_params = _read_env_store(bot_id) + success = docker_manager.start_bot( + bot_id, + image_tag=bot.image_tag, + on_state_change=docker_callback, + env_vars=env_params, + ) + if success: + bot.docker_status = "RUNNING" + session.add(bot) + session.commit() + return {"status": "started"} + + raise HTTPException(status_code=500, detail=f"Failed to start container with image {bot.image_tag}") + + +@app.post("/api/bots/{bot_id}/stop") +def stop_bot(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + docker_manager.stop_bot(bot_id) + bot.docker_status = "STOPPED" + session.add(bot) + session.commit() + return {"status": "stopped"} + + +@app.post("/api/bots/{bot_id}/deactivate") +def deactivate_bot(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + docker_manager.stop_bot(bot_id) + bot.docker_status = "STOPPED" + session.add(bot) + session.commit() + return {"status": "deactivated"} + + +@app.delete("/api/bots/{bot_id}") +def delete_bot(bot_id: str, delete_workspace: bool = True, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + docker_manager.stop_bot(bot_id) + + messages = session.exec(select(BotMessage).where(BotMessage.bot_id == bot_id)).all() + for row in messages: + session.delete(row) + + session.delete(bot) + session.commit() + + if delete_workspace: + workspace_root = os.path.join(BOTS_WORKSPACE_ROOT, bot_id) + if os.path.isdir(workspace_root): + shutil.rmtree(workspace_root, ignore_errors=True) + + return {"status": "deleted", "workspace_deleted": bool(delete_workspace)} + + +@app.get("/api/bots/{bot_id}/channels") +def list_bot_channels(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + return _get_bot_channels_from_config(bot) + + +@app.get("/api/bots/{bot_id}/skills") +def list_bot_skills(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + return _list_workspace_skills(bot_id) + + +@app.get("/api/bots/{bot_id}/tools-config") +def get_bot_tools_config(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + return { + "bot_id": bot_id, + "tools_config": _parse_tools_config(bot.tools_config_json), + } + + +@app.put("/api/bots/{bot_id}/tools-config") +def update_bot_tools_config(bot_id: str, payload: BotToolsConfigUpdateRequest, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + normalized = _normalize_tools_config(payload.tools_config) + bot.tools_config_json = json.dumps(normalized, ensure_ascii=False) + session.add(bot) + session.commit() + session.refresh(bot) + + _sync_workspace_channels(session, bot_id) + return { + "status": "updated", + "bot_id": bot_id, + "tools_config": normalized, + } + + +@app.get("/api/bots/{bot_id}/env-params") +def get_bot_env_params(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + return { + "bot_id": bot_id, + "env_params": _read_env_store(bot_id), + } + + +@app.put("/api/bots/{bot_id}/env-params") +def update_bot_env_params(bot_id: str, payload: BotEnvParamsUpdateRequest, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + normalized = _normalize_env_params(payload.env_params) + _write_env_store(bot_id, normalized) + return { + "status": "updated", + "bot_id": bot_id, + "env_params": normalized, + "restart_required": True, + } + + +@app.post("/api/bots/{bot_id}/skills/upload") +async def upload_bot_skill_zip(bot_id: str, file: UploadFile = File(...), session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + filename = str(file.filename or "").strip() + if not filename.lower().endswith(".zip"): + raise HTTPException(status_code=400, detail="Only .zip skill package is supported") + + try: + zip_bytes = await file.read() + if not zip_bytes: + raise HTTPException(status_code=400, detail="Zip package is empty") + archive = zipfile.ZipFile(io.BytesIO(zip_bytes)) + except Exception: + raise HTTPException(status_code=400, detail="Invalid zip file") + + skills_root = _skills_root(bot_id) + os.makedirs(skills_root, exist_ok=True) + + installed: List[str] = [] + with archive: + members = archive.infolist() + file_members = [m for m in members if not m.is_dir()] + if not file_members: + raise HTTPException(status_code=400, detail="Zip package has no files") + + top_names: List[str] = [] + for member in file_members: + raw_name = str(member.filename or "").replace("\\", "/").lstrip("/") + if not raw_name: + continue + first = raw_name.split("/", 1)[0].strip() + if not _is_valid_top_level_skill_name(first): + raise HTTPException(status_code=400, detail=f"Invalid skill entry name in zip: {first}") + if first not in top_names: + top_names.append(first) + + if not top_names: + raise HTTPException(status_code=400, detail="Zip package has no valid skill entries") + + conflicts = [name for name in top_names if os.path.exists(os.path.join(skills_root, name))] + if conflicts: + raise HTTPException(status_code=400, detail=f"Skill already exists: {', '.join(conflicts)}") + + with tempfile.TemporaryDirectory(prefix=".skill_upload_", dir=skills_root) as tmp_dir: + tmp_root = os.path.abspath(tmp_dir) + for member in members: + raw_name = str(member.filename or "").replace("\\", "/").lstrip("/") + if not raw_name: + continue + target = os.path.abspath(os.path.join(tmp_root, raw_name)) + if os.path.commonpath([tmp_root, target]) != tmp_root: + raise HTTPException(status_code=400, detail=f"Unsafe zip entry path: {raw_name}") + if member.is_dir(): + os.makedirs(target, exist_ok=True) + continue + os.makedirs(os.path.dirname(target), exist_ok=True) + with archive.open(member, "r") as source, open(target, "wb") as dest: + shutil.copyfileobj(source, dest) + + for name in top_names: + src = os.path.join(tmp_root, name) + dst = os.path.join(skills_root, name) + if not os.path.exists(src): + continue + shutil.move(src, dst) + installed.append(name) + + if not installed: + raise HTTPException(status_code=400, detail="No skill entries installed from zip") + + return { + "status": "installed", + "bot_id": bot_id, + "installed": installed, + "skills": _list_workspace_skills(bot_id), + } + + +@app.delete("/api/bots/{bot_id}/skills/{skill_name}") +def delete_bot_skill(bot_id: str, skill_name: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + name = str(skill_name or "").strip() + if not _is_valid_top_level_skill_name(name): + raise HTTPException(status_code=400, detail="Invalid skill name") + root = _skills_root(bot_id) + target = os.path.abspath(os.path.join(root, name)) + if os.path.commonpath([os.path.abspath(root), target]) != os.path.abspath(root): + raise HTTPException(status_code=400, detail="Invalid skill path") + if not os.path.exists(target): + raise HTTPException(status_code=404, detail="Skill not found in workspace") + if os.path.isdir(target): + shutil.rmtree(target, ignore_errors=False) + else: + os.remove(target) + return {"status": "deleted", "bot_id": bot_id, "skill": name} + + +@app.post("/api/bots/{bot_id}/channels") +def create_bot_channel(bot_id: str, payload: ChannelConfigRequest, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + ctype = (payload.channel_type or "").strip().lower() + if not ctype: + raise HTTPException(status_code=400, detail="channel_type is required") + if ctype == "dashboard": + raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be created manually") + current_rows = _get_bot_channels_from_config(bot) + if any(str(row.get("channel_type") or "").lower() == ctype for row in current_rows): + raise HTTPException(status_code=400, detail=f"Channel already exists: {ctype}") + + new_row = { + "id": ctype, + "bot_id": bot_id, + "channel_type": ctype, + "external_app_id": (payload.external_app_id or "").strip() or f"{ctype}-{bot_id}", + "app_secret": (payload.app_secret or "").strip(), + "internal_port": max(1, min(int(payload.internal_port or 8080), 65535)), + "is_active": bool(payload.is_active), + "extra_config": _normalize_channel_extra(payload.extra_config), + "locked": False, + } + + config_data = _read_bot_config(bot_id) + channels_cfg = config_data.get("channels") + if not isinstance(channels_cfg, dict): + channels_cfg = {} + config_data["channels"] = channels_cfg + channels_cfg[ctype] = _channel_api_to_cfg(new_row) + _write_bot_config(bot_id, config_data) + _sync_workspace_channels(session, bot_id) + return new_row + + +@app.put("/api/bots/{bot_id}/channels/{channel_id}") +def update_bot_channel( + bot_id: str, + channel_id: str, + payload: ChannelConfigUpdateRequest, + session: Session = Depends(get_session), +): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + channel_key = str(channel_id or "").strip().lower() + rows = _get_bot_channels_from_config(bot) + row = next((r for r in rows if str(r.get("id") or "").lower() == channel_key), None) + if not row: + raise HTTPException(status_code=404, detail="Channel not found") + + update_data = payload.model_dump(exclude_unset=True) + existing_type = str(row.get("channel_type") or "").strip().lower() + new_type = existing_type + if "channel_type" in update_data and update_data["channel_type"] is not None: + new_type = str(update_data["channel_type"]).strip().lower() + if not new_type: + raise HTTPException(status_code=400, detail="channel_type cannot be empty") + if existing_type == "dashboard" and new_type != "dashboard": + raise HTTPException(status_code=400, detail="dashboard channel type cannot be changed") + if new_type != existing_type and any(str(r.get("channel_type") or "").lower() == new_type for r in rows): + raise HTTPException(status_code=400, detail=f"Channel already exists: {new_type}") + + if "external_app_id" in update_data and update_data["external_app_id"] is not None: + row["external_app_id"] = str(update_data["external_app_id"]).strip() + if "app_secret" in update_data and update_data["app_secret"] is not None: + row["app_secret"] = str(update_data["app_secret"]).strip() + if "internal_port" in update_data and update_data["internal_port"] is not None: + row["internal_port"] = max(1, min(int(update_data["internal_port"]), 65535)) + if "is_active" in update_data and update_data["is_active"] is not None: + next_active = bool(update_data["is_active"]) + if existing_type == "dashboard" and not next_active: + raise HTTPException(status_code=400, detail="dashboard channel must remain enabled") + row["is_active"] = next_active + if "extra_config" in update_data: + row["extra_config"] = _normalize_channel_extra(update_data.get("extra_config")) + row["channel_type"] = new_type + row["id"] = new_type + row["locked"] = new_type == "dashboard" + + if new_type == "dashboard": + extra = _normalize_channel_extra(row.get("extra_config")) + bot.send_progress = bool(extra.get("sendProgress", bot.send_progress)) + bot.send_tool_hints = bool(extra.get("sendToolHints", bot.send_tool_hints)) + session.add(bot) + + config_data = _read_bot_config(bot_id) + channels_cfg = config_data.get("channels") + if not isinstance(channels_cfg, dict): + channels_cfg = {} + config_data["channels"] = channels_cfg + channels_cfg["sendProgress"] = bool(bot.send_progress) + channels_cfg["sendToolHints"] = bool(bot.send_tool_hints) + if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type: + channels_cfg.pop(existing_type, None) + if new_type != "dashboard": + channels_cfg[new_type] = _channel_api_to_cfg(row) + _write_bot_config(bot_id, config_data) + session.commit() + _sync_workspace_channels(session, bot_id) + return row + + +@app.delete("/api/bots/{bot_id}/channels/{channel_id}") +def delete_bot_channel(bot_id: str, channel_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + channel_key = str(channel_id or "").strip().lower() + rows = _get_bot_channels_from_config(bot) + row = next((r for r in rows if str(r.get("id") or "").lower() == channel_key), None) + if not row: + raise HTTPException(status_code=404, detail="Channel not found") + if str(row.get("channel_type") or "").lower() == "dashboard": + raise HTTPException(status_code=400, detail="dashboard channel cannot be deleted") + + config_data = _read_bot_config(bot_id) + channels_cfg = config_data.get("channels") + if not isinstance(channels_cfg, dict): + channels_cfg = {} + config_data["channels"] = channels_cfg + channels_cfg.pop(str(row.get("channel_type") or "").lower(), None) + _write_bot_config(bot_id, config_data) + session.commit() + _sync_workspace_channels(session, bot_id) + return {"status": "deleted"} + + +@app.post("/api/bots/{bot_id}/command") +def send_command(bot_id: str, payload: CommandRequest, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + attachments = _normalize_media_list(payload.attachments, bot_id) + command = str(payload.command or "").strip() + if not command and not attachments: + raise HTTPException(status_code=400, detail="Command or attachments is required") + + checked_attachments: List[str] = [] + for rel in attachments: + _, target = _resolve_workspace_path(bot_id, rel) + if not os.path.isfile(target): + raise HTTPException(status_code=400, detail=f"attachment not found: {rel}") + checked_attachments.append(rel) + + display_command = command if command else "[attachment message]" + delivery_command = display_command + if checked_attachments: + command_has_paths = all(p in command for p in checked_attachments) if command else False + attachment_block = "\n".join(f"- {p}" for p in checked_attachments) + if command and not command_has_paths: + delivery_command = ( + f"{command}\n\n" + "[Attached files]\n" + f"{attachment_block}\n\n" + "Please process the attached file(s) listed above when answering this request." + ) + elif not command: + delivery_command = ( + "Please process the uploaded file(s) listed below:\n" + f"{attachment_block}" + ) + + if display_command or checked_attachments: + _persist_runtime_packet( + bot_id, + {"type": "USER_COMMAND", "channel": "dashboard", "text": display_command, "media": checked_attachments}, + ) + + loop = getattr(app.state, "main_loop", None) + if loop and loop.is_running(): + asyncio.run_coroutine_threadsafe( + manager.broadcast( + bot_id, + { + "type": "USER_COMMAND", + "channel": "dashboard", + "text": display_command, + "media": checked_attachments, + }, + ), + loop, + ) + + success = docker_manager.send_command(bot_id, delivery_command, media=checked_attachments) + if not success: + if loop and loop.is_running(): + asyncio.run_coroutine_threadsafe( + manager.broadcast( + bot_id, + { + "type": "AGENT_STATE", + "channel": "dashboard", + "payload": { + "state": "ERROR", + "action_msg": "command delivery failed", + }, + }, + ), + loop, + ) + raise HTTPException(status_code=502, detail="Failed to deliver command to bot dashboard channel") + return {"success": True} + + +@app.get("/api/bots/{bot_id}/messages") +def list_bot_messages(bot_id: str, limit: int = 200, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + safe_limit = max(1, min(int(limit), 500)) + rows = session.exec( + select(BotMessage) + .where(BotMessage.bot_id == bot_id) + .order_by(BotMessage.created_at.desc(), BotMessage.id.desc()) + .limit(safe_limit) + ).all() + ordered = list(reversed(rows)) + return [ + { + "id": row.id, + "bot_id": row.bot_id, + "role": row.role, + "text": row.text, + "media": _parse_message_media(bot_id, getattr(row, "media_json", None)), + "ts": int(row.created_at.timestamp() * 1000), + } + for row in ordered + ] + + +@app.delete("/api/bots/{bot_id}/messages") +def clear_bot_messages(bot_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + rows = session.exec(select(BotMessage).where(BotMessage.bot_id == bot_id)).all() + deleted = 0 + for row in rows: + session.delete(row) + deleted += 1 + bot.last_action = "" + bot.current_state = "IDLE" + bot.updated_at = datetime.utcnow() + session.add(bot) + session.commit() + return {"bot_id": bot_id, "deleted": deleted} + + +@app.get("/api/bots/{bot_id}/logs") +def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + return {"bot_id": bot_id, "logs": docker_manager.get_recent_logs(bot_id, tail=tail)} + + +@app.get("/api/bots/{bot_id}/workspace/tree") +def get_workspace_tree(bot_id: str, path: Optional[str] = None, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + root = _workspace_root(bot_id) + if not os.path.isdir(root): + return {"bot_id": bot_id, "root": root, "cwd": "", "parent": None, "entries": []} + + _, target = _resolve_workspace_path(bot_id, path) + if not os.path.isdir(target): + raise HTTPException(status_code=400, detail="workspace path is not a directory") + cwd = os.path.relpath(target, root).replace("\\", "/") + if cwd == ".": + cwd = "" + parent = None + if cwd: + parent = os.path.dirname(cwd).replace("\\", "/") + if parent == ".": + parent = "" + + return { + "bot_id": bot_id, + "root": root, + "cwd": cwd, + "parent": parent, + "entries": _list_workspace_dir(target, root), + } + + +@app.get("/api/bots/{bot_id}/workspace/file") +def read_workspace_file( + bot_id: str, + path: str, + max_bytes: int = 200000, + session: Session = Depends(get_session), +): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + + root, target = _resolve_workspace_path(bot_id, path) + if not os.path.isfile(target): + raise HTTPException(status_code=404, detail="workspace file not found") + + ext = os.path.splitext(target)[1].lower() + text_ext = { + "", + ".md", + ".txt", + ".log", + ".json", + ".yaml", + ".yml", + ".cfg", + ".ini", + ".csv", + ".tsv", + ".toml", + ".py", + ".sh", + } + if ext not in text_ext: + raise HTTPException(status_code=400, detail=f"unsupported file type: {ext or '(none)'}") + + safe_max = max(4096, min(int(max_bytes), 1000000)) + with open(target, "rb") as f: + raw = f.read(safe_max + 1) + + if b"\x00" in raw: + raise HTTPException(status_code=400, detail="binary file is not previewable") + + truncated = len(raw) > safe_max + body = raw[:safe_max] if truncated else raw + text_body = body.decode("utf-8", errors="replace") + rel_path = os.path.relpath(target, root).replace("\\", "/") + + return { + "bot_id": bot_id, + "path": rel_path, + "size": os.path.getsize(target), + "is_markdown": rel_path.lower().endswith(".md"), + "truncated": truncated, + "content": text_body, + } + + +@app.get("/api/bots/{bot_id}/cron/jobs") +def list_cron_jobs(bot_id: str, include_disabled: bool = True, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + store = _read_cron_store(bot_id) + rows = [] + for row in store.get("jobs", []): + if not isinstance(row, dict): + continue + enabled = bool(row.get("enabled", True)) + if not include_disabled and not enabled: + continue + rows.append(row) + rows.sort(key=lambda v: int(((v.get("state") or {}).get("nextRunAtMs")) or 2**62)) + return {"bot_id": bot_id, "version": int(store.get("version", 1) or 1), "jobs": rows} + + +@app.post("/api/bots/{bot_id}/cron/jobs/{job_id}/stop") +def stop_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + store = _read_cron_store(bot_id) + jobs = store.get("jobs", []) + if not isinstance(jobs, list): + jobs = [] + found = None + for row in jobs: + if isinstance(row, dict) and str(row.get("id")) == job_id: + found = row + break + if not found: + raise HTTPException(status_code=404, detail="Cron job not found") + found["enabled"] = False + found["updatedAtMs"] = int(datetime.utcnow().timestamp() * 1000) + _write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs}) + return {"status": "stopped", "job_id": job_id} + + +@app.delete("/api/bots/{bot_id}/cron/jobs/{job_id}") +def delete_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + store = _read_cron_store(bot_id) + jobs = store.get("jobs", []) + if not isinstance(jobs, list): + jobs = [] + kept = [row for row in jobs if not (isinstance(row, dict) and str(row.get("id")) == job_id)] + if len(kept) == len(jobs): + raise HTTPException(status_code=404, detail="Cron job not found") + _write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": kept}) + return {"status": "deleted", "job_id": job_id} + + +@app.get("/api/bots/{bot_id}/workspace/download") +def download_workspace_file( + bot_id: str, + path: str, + download: 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") + + _, target = _resolve_workspace_path(bot_id, path) + if not os.path.isfile(target): + raise HTTPException(status_code=404, detail="workspace file not found") + media_type, _ = mimetypes.guess_type(target) + if download: + return FileResponse(target, filename=os.path.basename(target), media_type=media_type) + return FileResponse(target, media_type=media_type) + + +@app.post("/api/bots/{bot_id}/workspace/upload") +async def upload_workspace_files( + bot_id: str, + files: List[UploadFile] = File(...), + path: Optional[str] = None, + session: Session = Depends(get_session), +): + bot = session.get(BotInstance, bot_id) + if not bot: + raise HTTPException(status_code=404, detail="Bot not found") + if not files: + raise HTTPException(status_code=400, detail="no files uploaded") + + root, upload_dir = _resolve_workspace_path(bot_id, path or "uploads") + os.makedirs(upload_dir, exist_ok=True) + safe_dir_real = os.path.abspath(upload_dir) + if os.path.commonpath([root, safe_dir_real]) != root: + raise HTTPException(status_code=400, detail="invalid upload target path") + + rows: List[Dict[str, Any]] = [] + for upload in files: + original = (upload.filename or "upload.bin").strip() or "upload.bin" + name = os.path.basename(original).replace("\\", "_").replace("/", "_") + name = re.sub(r"[^\w.\-()+@ ]+", "_", name) + if not name: + name = "upload.bin" + + abs_path = os.path.join(safe_dir_real, name) + if os.path.exists(abs_path): + base, ext = os.path.splitext(name) + name = f"{base}-{int(datetime.utcnow().timestamp())}{ext}" + abs_path = os.path.join(safe_dir_real, name) + + content = await upload.read() + with open(abs_path, "wb") as f: + f.write(content) + + rel = os.path.relpath(abs_path, root).replace("\\", "/") + rows.append({"name": name, "path": rel, "size": len(content)}) + + return {"bot_id": bot_id, "files": rows} + + +@app.websocket("/ws/monitor/{bot_id}") +async def websocket_endpoint(websocket: WebSocket, bot_id: str): + await manager.connect(bot_id, websocket) + docker_manager.ensure_monitor(bot_id, docker_callback) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(bot_id, websocket) + + +def _main_server_options() -> tuple[str, int, bool]: + host = str(os.getenv("APP_HOST", "0.0.0.0") or "0.0.0.0").strip() or "0.0.0.0" + try: + port = int(os.getenv("APP_PORT", "8000")) + except Exception: + port = 8000 + port = max(1, min(port, 65535)) + reload_flag = str(os.getenv("APP_RELOAD", "true")).strip().lower() in {"1", "true", "yes", "on"} + return host, port, reload_flag + + +if __name__ == "__main__": + import uvicorn + + host, port, reload_flag = _main_server_options() + app_module = f"{os.path.splitext(os.path.basename(__file__))[0]}:app" + if reload_flag: + uvicorn.run(app_module, host=host, port=port, reload=True) + else: + uvicorn.run(app, host=host, port=port) diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/bot.py b/backend/models/bot.py new file mode 100644 index 0000000..401c358 --- /dev/null +++ b/backend/models/bot.py @@ -0,0 +1,50 @@ +from typing import Optional +from sqlmodel import Field, SQLModel +from datetime import datetime + +class BotInstance(SQLModel, table=True): + id: str = Field(primary_key=True) + name: str + avatar_model: str = Field(default="base") + avatar_skin: Optional[str] = Field(default="blue_suit") + system_prompt: Optional[str] = Field(default=None) + soul_md: Optional[str] = Field(default=None) + agents_md: Optional[str] = Field(default=None) + user_md: Optional[str] = Field(default=None) + tools_md: Optional[str] = Field(default=None) + tools_config_json: Optional[str] = Field(default=None) # JSON string for runtime tools config + identity_md: Optional[str] = Field(default=None) + llm_provider: str + llm_model: str + api_key: str # Encrypted + api_base: Optional[str] = Field(default=None) + temperature: float = Field(default=0.2) + top_p: float = Field(default=1.0) + max_tokens: int = Field(default=8192) + presence_penalty: float = Field(default=0.0) + frequency_penalty: float = Field(default=0.0) + send_progress: bool = Field(default=False) + send_tool_hints: bool = Field(default=False) + workspace_dir: str = Field(unique=True) + docker_status: str = Field(default="STOPPED", index=True) + current_state: Optional[str] = Field(default="IDLE") + last_action: Optional[str] = Field(default=None) + image_tag: str = Field(default="nanobot-base:v0.1.4") # 记录该机器人使用的镜像版本 + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + +class BotMessage(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + bot_id: str = Field(foreign_key="botinstance.id", index=True) + role: str = Field(index=True) # user | assistant | system + text: str + media_json: Optional[str] = Field(default=None) # JSON string list of workspace-relative file paths + created_at: datetime = Field(default_factory=datetime.utcnow, index=True) + +class NanobotImage(SQLModel, table=True): + tag: str = Field(primary_key=True) # e.g., nanobot-base:v0.1.4 + image_id: Optional[str] = Field(default=None) # Docker 内部的 Image ID + version: str # e.g., 0.1.4 + status: str = Field(default="READY") # READY, BUILDING, ERROR + source_dir: Optional[str] = Field(default=None) # 本地来源标识(如 manual) + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..bbacb10 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,16 @@ +fastapi==0.110.0 +uvicorn==0.27.1 +docker==7.0.0 +sqlmodel==0.0.16 +pydantic==2.6.3 +psycopg[binary]==3.2.1 +pymysql==1.1.1 +python-dotenv==1.0.1 +httpx==0.27.0 +websockets==12.0 +python-multipart==0.0.9 +cryptography==42.0.5 +jinja2==3.1.3 +watchfiles==0.21.0 +urllib3==1.26.18 +requests==2.31.0 diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot-images/Dashboard.Dockerfile b/bot-images/Dashboard.Dockerfile new file mode 100644 index 0000000..1e15d17 --- /dev/null +++ b/bot-images/Dashboard.Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.12-slim +ENV PYTHONUNBUFFERED=1 +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV PYTHONIOENCODING=utf-8 + +# 1. 替换 apt 国内源 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources || \ + sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list + +# 2. 安装基础依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 3. 安装 aiohttp 和基础 python 工具 +RUN python -m pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --upgrade \ + pip setuptools wheel aiohttp + +WORKDIR /app +# 这一步会把您修改好的 nanobot/channels/dashboard.py 一起拷进去 +COPY . /app + +# 4. 安装 nanobot +RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ . + +WORKDIR /root +# 官方 gateway 模式,现在它会自动加载您的 DashboardChannel +CMD ["nanobot", "gateway"] diff --git a/design/architecture.md b/design/architecture.md new file mode 100644 index 0000000..951c26c --- /dev/null +++ b/design/architecture.md @@ -0,0 +1,64 @@ +# Dashboard Nanobot 架构设计文档(当前实现) + +## 1. 设计原则 + +- 控制平面与数据平面分离。 +- 引擎零侵入:不修改 nanobot 源码,仅通过 workspace 与容器管理接入。 +- 镜像显式登记:系统不自动构建,不扫描 `engines/`,只使用 Docker 本地镜像 + DB 注册。 +- 可观测性优先:通过容器日志流解析状态并推送到 WebSocket。 + +## 2. 核心组件 + +### 2.1 Frontend(React + Zustand) + +- `ImageFactoryModule`:镜像登记与状态展示。 +- `BotWizardModule`:引导创建与模型测试。 +- `BotDashboardModule`:2D 运维控制台(无 3D),采用标准 LLM 聊天气泡视图 + 对话流中的思考动画 + workspace 文件浏览器。 +- 全局支持 light/dark 与 中英文切换。 + +### 2.2 Backend(FastAPI + SQLModel + docker-py) + +- 镜像 API:`/api/images`、`/api/docker-images`、`/api/images/register` +- Bot 生命周期 API:`/api/bots`、`/api/bots/{id}/start|stop|command` +- Workspace API:`/api/bots/{id}/workspace/tree`、`/api/bots/{id}/workspace/file` +- Provider 测试 API:`/api/providers/test` +- 监控通道:`/ws/monitor/{bot_id}` + +### 2.3 Workspace 配置生成器 + +`BotConfigManager.update_workspace` 负责: + +- 写入 `.nanobot/config.json` +- 写入 5 个 bootstrap 文件: + - `AGENTS.md` + - `SOUL.md` + - `USER.md` + - `TOOLS.md` + - `IDENTITY.md` + +## 3. 关键数据流 + +### 3.1 镜像流 + +1. 用户手动构建镜像(命令行) +2. 前端读取 `/api/docker-images` +3. 用户点击“加入数据库” -> `/api/images/register` +4. 后续创建向导只消费 `/api/images` 且筛选 `READY` + +### 3.2 创建流 + +1. 前端提交 Bot 创建参数(镜像、provider、模型参数、5 个 md 文件) +2. 后端校验镜像可用并写入 `botinstance` +3. 启动时生成 workspace 文件并拉起容器 + +### 3.3 运维流 + +1. 前端轮询 `/api/bots` 获取实例列表 +2. 对运行中 bot 建立 WebSocket 连接 +3. 后端解析日志,推送 `AGENT_STATE` 与 `RAW_LOG` + +## 4. 当前约束 + +- Provider 测试接口当前只做 `dashscope/openrouter`。 +- Skills 采用 workspace 文件系统模式管理,不再使用数据库注册表。 +- Bot 状态来自日志与容器状态,不依赖侵入式埋点。 diff --git a/design/dashboard-nanobot.md b/design/dashboard-nanobot.md new file mode 100644 index 0000000..4d59116 --- /dev/null +++ b/design/dashboard-nanobot.md @@ -0,0 +1,131 @@ +# Dashboard Nanobot 技术规范书(当前实现) + +本文档描述当前代码已落地的 Dashboard Nanobot 控制平面规范,覆盖前端流程、后端接口、配置文件生成和运行约束。 + +## 1. 产品目标 + +系统围绕三步主流程: + +1. 镜像工厂:仅管理 Docker 本地镜像与数据库登记状态。 +2. 引导创建:基于 READY 镜像创建 Bot,填写模型参数与代理文件。 +3. 运维 Dashboard:2D 控制台管理 Bot 对话、状态、日志与启停。 + +## 2. 前端模块规范 + +### 2.1 镜像工厂(Image Factory) + +- 页面显示两类列表: + - 数据库已登记镜像(来源 `/api/images`) + - Docker 本地镜像(来源 `/api/docker-images`) +- 系统不再自动扫描 `engines/`。 +- 仅通过“加入数据库”动作调用 `/api/images/register` 完成登记。 +- 创建向导仅允许使用 `status=READY` 的已登记镜像。 + +### 2.2 引导创建(Bot Wizard) + +- 采用 4 步流程: + 1. 选择 READY 镜像 + 2. 模型与参数 + 3. 代理文件配置 + 4. 确认创建 +- Provider 支持:`dashscope`、`openrouter`、`openai`、`deepseek`。 +- 提供模型连通性测试按钮(调用 `/api/providers/test`)。 +- 标准模型参数: + - `temperature` + - `top_p` + - `max_tokens` +- 参数编辑交互: + - `temperature`、`top_p` 使用滑轨 + - `max_tokens` 使用数字输入 +- 代理文件配置项(全部可编辑并持久化): + - `AGENTS.md` + - `SOUL.md` + - `USER.md` + - `TOOLS.md` + - `IDENTITY.md` +- 渠道配置: + - 创建向导只配置“可选外部渠道”(飞书/QQ 等) + - `dashboard` 渠道为内置渠道,不在向导中生成渠道块 + - 全局 `sendProgress/sendToolHints` 作为独立开关提交 + +### 2.3 运维 Dashboard(2D) + +- 不使用 3D 场景。 +- 三栏布局: + - 左侧:Bot 列表、镜像、启停操作 + - 中间:标准 LLM 对话区(Bot 左气泡 + 用户右气泡);Bot 处于 THINKING 时在对话流中显示思考动画 + - 右侧:运行遥测、最后反馈、workspace 文件浏览器(双击进目录/返回上级/双击文件弹窗预览) +- 支持实时状态更新(WebSocket `/ws/monitor/{bot_id}`)。 +- Skills 管理入口仅在运维面板(更多菜单): + - 列出当前 Bot 的 `workspace/skills` 内容 + - 支持删除技能目录/文件 + - 支持上传 zip 并安全解压到 `workspace/skills` +- workspace 可视化接口: + - `/api/bots/{bot_id}/workspace/tree` + - `/api/bots/{bot_id}/workspace/file` + +### 2.4 主题与语言 + +- 支持 `light/dark` 切换。 +- 支持 `中文/English` 切换。 +- 切换状态保存在浏览器 `localStorage`。 + +## 3. 后端行为规范 + +### 3.1 镜像登记策略 + +- `/api/images`:只返回数据库登记镜像,并和 Docker 实时对账更新状态。 +- `/api/docker-images`:列出本地 `nanobot-base:*`。 +- `/api/images/register`:将本地镜像显式写入登记表。 + +### 3.2 Bot 创建与启动 + +- `/api/bots` 创建时必须满足: + - 镜像已登记 + - 镜像状态 `READY` + - 本机 Docker 存在该镜像 +- `/api/bots/{id}/start` 启动前写入 workspace: + - `.nanobot/config.json` + - `.nanobot/workspace/AGENTS.md` + - `.nanobot/workspace/SOUL.md` + - `.nanobot/workspace/USER.md` + - `.nanobot/workspace/TOOLS.md` + - `.nanobot/workspace/IDENTITY.md` + +### 3.3 Provider 测试接口 + +- `/api/providers/test` 当前支持: + - `openrouter`(`/models`) + - `dashscope`(`/models`) +- 返回模型预览列表与匹配提示。 + +### 3.4 Skills 文件系统接口 + +- `GET /api/bots/{bot_id}/skills`:列出 `workspace/skills` 一级技能项。 +- `POST /api/bots/{bot_id}/skills/upload`:上传 zip,做路径安全校验后解压安装。 +- `DELETE /api/bots/{bot_id}/skills/{skill_name}`:删除指定技能目录/文件。 + +## 4. Workspace 规范 + +宿主机目录:`workspace/bots/{bot_id}/.nanobot/` + +```text +.nanobot/ +├── config.json +└── workspace/ + ├── AGENTS.md + ├── SOUL.md + ├── USER.md + ├── TOOLS.md + ├── IDENTITY.md + ├── memory/ + └── skills/ +``` + +`skills/` 目录下每个一级目录(或单文件)视为一个可管理技能单元。 + +## 5. 与 nanobot 的兼容原则 + +- 不侵入 nanobot 源码。 +- 控制平面通过文件与容器生命周期管理进行编排。 +- Bootstrap 文件命名与 `nanobot.agent.context.ContextBuilder.BOOTSTRAP_FILES` 对齐。 diff --git a/design/database.md b/design/database.md new file mode 100644 index 0000000..3307ac4 --- /dev/null +++ b/design/database.md @@ -0,0 +1,130 @@ +# Dashboard Nanobot 数据库设计文档(同步到当前代码) + +数据库默认使用 SQLite:`data/nanobot_dashboard.db`。 + +## 1. ERD + +```mermaid +erDiagram + BOTINSTANCE ||--o{ CHANNELROUTE : "路由" + BOTINSTANCE ||--o{ BOTMESSAGE : "消息" + NANOBOTIMAGE ||--o{ BOTINSTANCE : "被引用" + + BOTINSTANCE { + string id PK + string name + text system_prompt + text soul_md + text agents_md + text user_md + text tools_md + text identity_md + text tools_config_json + string llm_provider + string llm_model + string api_key + string api_base + float temperature + float top_p + int max_tokens + float presence_penalty + float frequency_penalty + string workspace_dir UK + string docker_status + string image_tag + string current_state + text last_action + datetime created_at + datetime updated_at + } + + BOTMESSAGE { + int id PK + string bot_id FK + string role + text text + text media_json + datetime created_at + } + + NANOBOTIMAGE { + string tag PK + string image_id + string version + string status + string source_dir + datetime created_at + } + + CHANNELROUTE { + int id PK + string bot_id FK + string channel_type + string external_app_id + string app_secret + text extra_config + int internal_port + boolean is_active + } +``` + +## 2. 表说明 + +### 2.1 `botinstance` + +存储机器人实例核心配置与运行参数。 + +- 运行与镜像:`docker_status`、`image_tag` +- LLM 基础:`llm_provider`、`llm_model`、`api_key`、`api_base` +- LLM 参数:`temperature`、`top_p`、`max_tokens` +- Agent Bootstrap 内容:`soul_md`、`agents_md`、`user_md`、`tools_md`、`identity_md` +- 工具配置:`tools_config_json` +- 运行态:`current_state`、`last_action` + +### 2.2 `botmessage` + +保存 Dashboard 渠道持久化对话消息。 + +- `role`:`user | assistant` +- `text`:消息正文 +- `media_json`:附件路径列表(JSON 字符串) + +### 2.3 `nanobotimage` + +镜像登记表(只记录显式登记镜像)。 + +- `tag`: 如 `nanobot-base:v0.1.4` +- `status`: `READY | BUILDING | ERROR | UNKNOWN` +- `source_dir`: 当前手工模式通常为 `manual` + +### 2.4 `channelroute` + +渠道路由映射表。 + +- `dashboard` 为内置渠道,不允许删除或禁用。 +- `extra_config` 保存各渠道差异化字段。 + +## 3. 已移除的数据表 + +以下旧表已废弃,并在服务启动时自动删除: + +- `skillregistry` +- `botskillmapping` + +技能管理改为 **workspace 文件系统模式**: + +- 路径:`workspace/bots/{bot_id}/.nanobot/workspace/skills/` +- 管理 API:`/api/bots/{bot_id}/skills`、`/api/bots/{bot_id}/skills/upload`、`/api/bots/{bot_id}/skills/{skill_name}` + +## 4. 启动迁移策略(当前实现) + +服务启动时自动执行: + +1. `SQLModel.metadata.create_all(engine)` +2. 删除旧 skills 表(`DROP TABLE IF EXISTS botskillmapping/skillregistry`) +3. 针对 `botinstance` 和 `botmessage` 做增量列补齐(`ALTER TABLE ADD COLUMN`) + +## 5. 安全与一致性 + +- `api_key`、`app_secret` 建议加密存储(当前代码为明文字段,生产需加密层)。 +- 启动 Bot 前,以数据库字段 + 渠道路由重新生成 workspace 文件,确保配置一致性。 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..d8202b3 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,75 @@ +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + args: + PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-python:3.12-slim} + PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple} + PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-} + image: dashboard-nanobot/backend:${BACKEND_IMAGE_TAG:-latest} + container_name: dashboard-nanobot-backend + restart: unless-stopped + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8000 + APP_RELOAD: "false" + DATABASE_ECHO: "false" + DATA_ROOT: ${HOST_DATA_ROOT} + BOTS_WORKSPACE_ROOT: ${HOST_BOTS_WORKSPACE_ROOT} + DATABASE_URL: ${DATABASE_URL:-} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${HOST_DATA_ROOT}:${HOST_DATA_ROOT} + - ${HOST_BOTS_WORKSPACE_ROOT}:${HOST_BOTS_WORKSPACE_ROOT} + expose: + - "8000" + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3).read()"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + networks: + - nanobot_net + + nginx: + build: + context: ./frontend + dockerfile: Dockerfile + args: + NODE_BASE_IMAGE: ${NODE_BASE_IMAGE:-node:20-alpine} + NGINX_BASE_IMAGE: ${NGINX_BASE_IMAGE:-nginx:alpine} + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/} + VITE_API_BASE: /api + VITE_WS_BASE: /ws/monitor + image: dashboard-nanobot/nginx:${FRONTEND_IMAGE_TAG:-latest} + container_name: dashboard-nanobot-nginx + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "${NGINX_PORT}:80" + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 10s + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + networks: + - nanobot_net + +networks: + nanobot_net: + driver: bridge diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..f6dddf3 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,5 @@ +# Backend API entry +VITE_API_BASE=http://localhost:8000/api + +# Backend WebSocket entry +VITE_WS_BASE=ws://localhost:8000/ws/monitor diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..b50664c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env +.env.* +!.env.example + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..84ea40c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,29 @@ +ARG NODE_BASE_IMAGE=node:20-alpine +FROM ${NODE_BASE_IMAGE} AS build + +WORKDIR /app + +ARG NPM_REGISTRY=https://registry.npmjs.org/ + +COPY package*.json ./ +RUN npm config set registry "${NPM_REGISTRY}" \ + && npm ci + +COPY . . + +ARG VITE_API_BASE=/api +ARG VITE_WS_BASE=/ws/monitor +ENV VITE_API_BASE=${VITE_API_BASE} +ENV VITE_WS_BASE=${VITE_WS_BASE} + +RUN npm run build + +ARG NGINX_BASE_IMAGE=nginx:alpine +FROM ${NGINX_BASE_IMAGE} + +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/docker/nginx.conf b/frontend/docker/nginx.conf new file mode 100644 index 0000000..62b92ed --- /dev/null +++ b/frontend/docker/nginx.conf @@ -0,0 +1,37 @@ +upstream nanobot_backend { + server backend:8000; +} + +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://nanobot_backend/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /ws/monitor/ { + proxy_pass http://nanobot_backend/ws/monitor/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..106c884 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Dashboard Nanobot + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b681fea --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", + "@types/three": "^0.183.1", + "axios": "^1.13.5", + "clsx": "^2.1.1", + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.5.0", + "three": "^0.183.1", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/app-bot-icon.svg b/frontend/public/app-bot-icon.svg new file mode 100644 index 0000000..caac460 --- /dev/null +++ b/frontend/public/app-bot-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/public/nanobot-logo.png b/frontend/public/nanobot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9493813e6ac231f219bb81cffc6fd7e09ed05607 GIT binary patch literal 151546 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
+
+
+
+
+ Nanobot +
+

{t.title}

+
+
+ +
+
+ + +
+ +
+ + +
+
+
+
+ +
+ setShowCreateWizard(true)} + onOpenImageFactory={() => setShowImageFactory(true)} + forcedBotId={urlView.forcedBotId || undefined} + compactMode={urlView.compactMode} + /> +
+
+ + {!urlView.compactMode && showImageFactory && ( +
setShowImageFactory(false)}> +
e.stopPropagation()}> +
+

{t.nav.images.title}

+ +
+
+ +
+
+
+ )} + + {!urlView.compactMode && showCreateWizard && ( +
setShowCreateWizard(false)}> +
e.stopPropagation()}> +
+

{t.nav.onboarding.title}

+ +
+
+ { + setShowCreateWizard(false); + }} + onGoDashboard={() => setShowCreateWizard(false)} + /> +
+
+
+ )} +
+ ); +} + +export default App; diff --git a/frontend/src/assets/nanobot-logo.png b/frontend/src/assets/nanobot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9493813e6ac231f219bb81cffc6fd7e09ed05607 GIT binary patch literal 151546 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 + + {confirmState ? ( +
closeConfirm(false)}> +
e.stopPropagation()}> +
+ + + +
+ {confirmState.title || (locale === 'zh' ? '请确认操作' : 'Please Confirm')} +
+ +
+
{confirmState.message}
+
+ + +
+
+
+ ) : null} + + ); +} + +export function useLucentPrompt() { + const ctx = useContext(LucentPromptContext); + if (!ctx) { + throw new Error('useLucentPrompt must be used inside LucentPromptProvider'); + } + return ctx; +} + diff --git a/frontend/src/components/lucent/lucent-prompt.css b/frontend/src/components/lucent/lucent-prompt.css new file mode 100644 index 0000000..a258556 --- /dev/null +++ b/frontend/src/components/lucent/lucent-prompt.css @@ -0,0 +1,146 @@ +.lucent-toast-stack { + position: fixed; + right: 16px; + top: 16px; + z-index: 1800; + display: grid; + gap: 10px; + width: min(360px, calc(100vw - 24px)); +} + +.lucent-toast { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px; + align-items: start; + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px 12px; + background: color-mix(in oklab, var(--panel) 78%, transparent); + backdrop-filter: blur(12px); + box-shadow: 0 12px 28px rgba(8, 14, 30, 0.26); +} + +.lucent-toast.tone-success { + border-color: rgba(87, 204, 146, 0.44); +} + +.lucent-toast.tone-warning { + border-color: rgba(242, 186, 92, 0.44); +} + +.lucent-toast.tone-error { + border-color: rgba(223, 109, 109, 0.44); +} + +.lucent-toast-icon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--icon); +} + +.lucent-toast-body { + min-width: 0; +} + +.lucent-toast-title { + font-size: 12px; + font-weight: 800; + color: var(--title); + margin-bottom: 3px; +} + +.lucent-toast-message { + font-size: 13px; + color: var(--text); + line-height: 1.45; + word-break: break-word; +} + +.lucent-confirm-mask { + position: fixed; + inset: 0; + z-index: 1900; + background: rgba(8, 13, 24, 0.52); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.lucent-confirm-card { + width: min(520px, calc(100vw - 24px)); + border: 1px solid var(--line); + border-radius: 14px; + background: color-mix(in oklab, var(--panel) 86%, transparent); + box-shadow: 0 16px 40px rgba(8, 14, 30, 0.36); + padding: 14px; + display: grid; + gap: 12px; +} + +.lucent-confirm-card.tone-warning { + border-color: rgba(242, 186, 92, 0.44); +} + +.lucent-confirm-card.tone-error { + border-color: rgba(223, 109, 109, 0.46); +} + +.lucent-confirm-card.tone-success { + border-color: rgba(87, 204, 146, 0.46); +} + +.lucent-confirm-head { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; +} + +.lucent-confirm-icon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--icon); +} + +.lucent-confirm-title { + font-size: 15px; + font-weight: 800; + color: var(--title); +} + +.lucent-confirm-close { + border: 1px solid var(--line); + border-radius: 8px; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + background: color-mix(in oklab, var(--panel-soft) 80%, var(--panel) 20%); + color: var(--icon); + cursor: pointer; +} + +.lucent-confirm-message { + color: var(--text); + font-size: 14px; + line-height: 1.6; + white-space: pre-wrap; +} + +.lucent-confirm-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts new file mode 100644 index 0000000..77c5afc --- /dev/null +++ b/frontend/src/config/env.ts @@ -0,0 +1,32 @@ +function browserOrigin() { + if (typeof window === 'undefined') return 'http://localhost:8000'; + return window.location.origin; +} + +function browserWsOrigin() { + if (typeof window === 'undefined') return 'ws://localhost:8000'; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}`; +} + +function normalizeApiBase(raw?: string) { + const value = String(raw || '').trim(); + if (!value) return `${browserOrigin()}/api`; + if (value.startsWith('/')) return value; + return value; +} + +function normalizeWsBase(raw?: string) { + const value = String(raw || '').trim(); + if (!value) return `${browserWsOrigin()}/ws/monitor`; + if (value.startsWith('/')) return `${browserWsOrigin()}${value}`; + return value; +} + +const apiBase = normalizeApiBase(import.meta.env.VITE_API_BASE); +const wsBase = normalizeWsBase(import.meta.env.VITE_WS_BASE); + +export const APP_ENDPOINTS = { + apiBase, + wsBase, +}; diff --git a/frontend/src/hooks/useBotsSync.ts b/frontend/src/hooks/useBotsSync.ts new file mode 100644 index 0000000..f5a967b --- /dev/null +++ b/frontend/src/hooks/useBotsSync.ts @@ -0,0 +1,242 @@ +import { useEffect, useRef } from 'react'; +import axios from 'axios'; +import { useAppStore } from '../store/appStore'; +import { APP_ENDPOINTS } from '../config/env'; +import type { BotState, ChatMessage } from '../types/bot'; +import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgressText } from '../modules/dashboard/messageParser'; +import { pickLocale } from '../i18n'; +import { botsSyncZhCn } from '../i18n/bots-sync.zh-cn'; +import { botsSyncEn } from '../i18n/bots-sync.en'; + +function normalizeState(v: string): 'THINKING' | 'TOOL_CALL' | 'SUCCESS' | 'ERROR' | 'INFO' { + const s = (v || '').toUpperCase(); + if (s === 'THINKING' || s === 'TOOL_CALL' || s === 'SUCCESS' || s === 'ERROR') return s; + return 'INFO'; +} + +function normalizeBusState(isTool: boolean): 'THINKING' | 'TOOL_CALL' { + return isTool ? 'TOOL_CALL' : 'THINKING'; +} + +function normalizeMedia(raw: unknown): string[] { + if (!Array.isArray(raw)) return []; + return raw.map((v) => String(v || '').trim()).filter((v) => v.length > 0); +} + +export function useBotsSync() { + const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore(); + const socketsRef = useRef>({}); + const heartbeatsRef = useRef>({}); + const lastUserEchoRef = useRef>({}); + const lastAssistantRef = useRef>({}); + const hydratedMessagesRef = useRef>({}); + const isZh = useAppStore((s) => s.locale === 'zh'); + const locale = useAppStore((s) => s.locale); + const t = pickLocale(locale, { 'zh-cn': botsSyncZhCn, en: botsSyncEn }); + + useEffect(() => { + const fetchBots = async () => { + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`); + setBots(res.data); + } catch (error) { + console.error('Failed to fetch bots', error); + } + }; + + fetchBots(); + const interval = window.setInterval(fetchBots, 5000); + + return () => { + window.clearInterval(interval); + }; + }, [setBots]); + + useEffect(() => { + const botIds = Object.keys(activeBots); + const aliveIds = new Set(botIds); + Object.keys(hydratedMessagesRef.current).forEach((botId) => { + if (!aliveIds.has(botId)) { + delete hydratedMessagesRef.current[botId]; + } + }); + + botIds.forEach((botId) => { + if (hydratedMessagesRef.current[botId]) return; + hydratedMessagesRef.current[botId] = true; + void (async () => { + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages`, { + params: { limit: 300 }, + }); + const rows = Array.isArray(res.data) ? res.data : []; + const messages: ChatMessage[] = rows + .map((row) => { + const roleRaw = String(row?.role || '').toLowerCase(); + const role: ChatMessage['role'] = roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system' ? roleRaw : 'assistant'; + return { + role, + text: String(row?.text || ''), + attachments: normalizeMedia(row?.media), + ts: Number(row?.ts || Date.now()), + }; + }) + .filter((msg) => msg.text.trim().length > 0 || (msg.attachments || []).length > 0) + .slice(-300); + setBotMessages(botId, messages); + + const lastUser = [...messages].reverse().find((m) => m.role === 'user'); + if (lastUser) lastUserEchoRef.current[botId] = { text: lastUser.text, ts: lastUser.ts }; + const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant'); + if (lastAssistant) lastAssistantRef.current[botId] = { text: lastAssistant.text, ts: lastAssistant.ts }; + } catch (error) { + console.error(`Failed to fetch bot messages for ${botId}`, error); + } + })(); + }); + }, [activeBots, setBotMessages]); + + useEffect(() => { + const runningIds = new Set( + Object.values(activeBots) + .filter((bot) => bot.docker_status === 'RUNNING') + .map((bot) => bot.id), + ); + + Object.keys(socketsRef.current).forEach((botId) => { + if (!runningIds.has(botId)) { + socketsRef.current[botId].close(); + delete socketsRef.current[botId]; + } + }); + + Object.values(activeBots).forEach((bot) => { + if (bot.docker_status !== 'RUNNING') { + return; + } + if (socketsRef.current[bot.id]) { + return; + } + + const ws = new WebSocket(`${APP_ENDPOINTS.wsBase}/${bot.id}`); + ws.onopen = () => { + const beat = window.setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send('ping'); + } + }, 15000); + heartbeatsRef.current[bot.id] = beat; + }; + ws.onmessage = (event) => { + let data: any; + try { + data = JSON.parse(event.data); + } catch { + return; + } + const sourceChannel = String(data?.channel || '').trim().toLowerCase(); + const isDashboardChannel = sourceChannel === 'dashboard'; + const payload = data?.payload && typeof data.payload === 'object' ? data.payload : {}; + if (data.type === 'AGENT_STATE') { + const state = String(payload.state || data.state || 'INFO'); + const messageRaw = String(payload.action_msg || payload.msg || data.action_msg || data.msg || ''); + const message = summarizeProgressText(messageRaw, isZh); + updateBotState(bot.id, state, message); + addBotEvent(bot.id, { + state: normalizeState(state), + text: message || t.stateUpdated, + ts: Date.now(), + channel: sourceChannel || undefined, + }); + return; + } + if (data.type === 'ASSISTANT_MESSAGE') { + if (!isDashboardChannel) return; + const text = normalizeAssistantMessageText(String(data.text || payload.text || payload.content || '')); + const attachments = normalizeMedia(data.media || payload.media); + if (!text && attachments.length === 0) return; + const now = Date.now(); + const prev = lastAssistantRef.current[bot.id]; + if (prev && prev.text === text && now - prev.ts < 5000 && attachments.length === 0) return; + lastAssistantRef.current[bot.id] = { text, ts: now }; + addBotMessage(bot.id, { role: 'assistant', text, attachments, ts: now, kind: 'final' }); + updateBotState(bot.id, 'IDLE', ''); + addBotEvent(bot.id, { state: 'SUCCESS', text: t.replied, ts: Date.now(), channel: sourceChannel || undefined }); + return; + } + if (data.type === 'BUS_EVENT') { + const content = normalizeAssistantMessageText(String(data.content || payload.content || '')); + const isProgress = Boolean(data.is_progress); + const isTool = Boolean(data.is_tool); + if (isProgress) { + const state = normalizeBusState(isTool); + const progressText = summarizeProgressText(content, isZh); + updateBotState(bot.id, state, progressText); + addBotEvent(bot.id, { state, text: progressText || t.progress, ts: Date.now(), channel: sourceChannel || undefined }); + if (isDashboardChannel && isTool) { + const chatText = + (isZh ? '调用工具:' : 'Tool: ') + (progressText || (isZh ? '处理中...' : 'processing...')); + addBotMessage(bot.id, { + role: 'assistant', + text: chatText, + ts: Date.now(), + kind: 'progress', + }); + } + return; + } + if (!isDashboardChannel) return; + if (content) { + const now = Date.now(); + const prev = lastAssistantRef.current[bot.id]; + if (!prev || prev.text !== content || now - prev.ts >= 5000) { + addBotMessage(bot.id, { role: 'assistant', text: content, ts: now, kind: 'final' }); + lastAssistantRef.current[bot.id] = { text: content, ts: now }; + } + updateBotState(bot.id, 'IDLE', summarizeProgressText(content, isZh)); + addBotEvent(bot.id, { state: 'SUCCESS', text: t.replied, ts: Date.now(), channel: sourceChannel || undefined }); + } + return; + } + if (data.type === 'USER_COMMAND') { + if (!isDashboardChannel) return; + const text = normalizeUserMessageText(String(data.text || payload.text || payload.command || '')); + const attachments = normalizeMedia(data.media || payload.media); + if (!text && attachments.length === 0) return; + const now = Date.now(); + const prev = lastUserEchoRef.current[bot.id]; + if (prev && prev.text === text && now - prev.ts < 10000 && attachments.length === 0) return; + lastUserEchoRef.current[bot.id] = { text, ts: now }; + addBotMessage(bot.id, { role: 'user', text, attachments, ts: now, kind: 'final' }); + return; + } + if (data.type === 'RAW_LOG') { + addBotLog(bot.id, String(data.text || '')); + } + }; + ws.onclose = () => { + const hb = heartbeatsRef.current[bot.id]; + if (hb) { + window.clearInterval(hb); + delete heartbeatsRef.current[bot.id]; + } + delete socketsRef.current[bot.id]; + }; + + socketsRef.current[bot.id] = ws; + }); + + return () => { + // no-op: clean in unmount effect below + }; + }, [activeBots, addBotEvent, addBotLog, addBotMessage, isZh, t.progress, t.replied, t.stateUpdated, updateBotState]); + + useEffect(() => { + return () => { + Object.values(socketsRef.current).forEach((ws) => ws.close()); + Object.values(heartbeatsRef.current).forEach((timerId) => window.clearInterval(timerId)); + heartbeatsRef.current = {}; + socketsRef.current = {}; + }; + }, []); +} diff --git a/frontend/src/i18n/README.md b/frontend/src/i18n/README.md new file mode 100644 index 0000000..fba78d7 --- /dev/null +++ b/frontend/src/i18n/README.md @@ -0,0 +1,23 @@ +# I18N Convention + +Use this convention for every new page/module: + +1. Add two language packs in this folder: +- `.zh-cn.ts` +- `.en.ts` + +2. Export plain objects/functions from both files with the same key shape. + +3. In component code, select pack via: + +```ts +import { pickLocale } from '../i18n'; +import { xxxZhCn } from '../i18n/xxx.zh-cn'; +import { xxxEn } from '../i18n/xxx.en'; + +const t = pickLocale(locale, { 'zh-cn': xxxZhCn, en: xxxEn }); +``` + +4. Do not add inline `isZh ? ... : ...` strings in page code. + +5. Keep dynamic text in translation pack as small helper functions. diff --git a/frontend/src/i18n/app.en.ts b/frontend/src/i18n/app.en.ts new file mode 100644 index 0000000..a8361ec --- /dev/null +++ b/frontend/src/i18n/app.en.ts @@ -0,0 +1,15 @@ +export const appEn = { + title: 'Nanobot Control Plane', + theme: 'Theme', + language: 'Language', + dark: 'Dark', + light: 'Light', + zh: 'Chinese', + en: 'English', + close: 'Close', + nav: { + images: { title: 'Image Factory', subtitle: 'Manage registered images' }, + onboarding: { title: 'Creation Wizard', subtitle: 'Create bot step-by-step' }, + dashboard: { title: 'Ops Dashboard', subtitle: 'Chat, status, logs, controls' }, + }, +}; diff --git a/frontend/src/i18n/app.zh-cn.ts b/frontend/src/i18n/app.zh-cn.ts new file mode 100644 index 0000000..c3cc93e --- /dev/null +++ b/frontend/src/i18n/app.zh-cn.ts @@ -0,0 +1,15 @@ +export const appZhCn = { + title: 'Nanobot 管理面板', + theme: '主题', + language: '语言', + dark: '深色', + light: '浅色', + zh: '中文', + en: 'English', + close: '关闭', + nav: { + images: { title: '镜像工厂', subtitle: '管理已登记镜像' }, + onboarding: { title: '创建向导', subtitle: '分步创建 Bot' }, + dashboard: { title: '运维 Dashboard', subtitle: '对话、状态、日志与启停' }, + }, +}; diff --git a/frontend/src/i18n/bots-sync.en.ts b/frontend/src/i18n/bots-sync.en.ts new file mode 100644 index 0000000..417d857 --- /dev/null +++ b/frontend/src/i18n/bots-sync.en.ts @@ -0,0 +1,5 @@ +export const botsSyncEn = { + stateUpdated: 'state updated', + replied: 'Replied', + progress: 'progress', +}; diff --git a/frontend/src/i18n/bots-sync.zh-cn.ts b/frontend/src/i18n/bots-sync.zh-cn.ts new file mode 100644 index 0000000..1a81e77 --- /dev/null +++ b/frontend/src/i18n/bots-sync.zh-cn.ts @@ -0,0 +1,5 @@ +export const botsSyncZhCn = { + stateUpdated: '状态已更新', + replied: '已回复', + progress: '处理中', +}; diff --git a/frontend/src/i18n/channels.en.ts b/frontend/src/i18n/channels.en.ts new file mode 100644 index 0000000..eecb96b --- /dev/null +++ b/frontend/src/i18n/channels.en.ts @@ -0,0 +1,33 @@ +export const channelsEn = { + wizardSectionTitle: 'Channel Configuration', + wizardSectionDesc: 'Manage custom channels.', + globalDeliveryTitle: 'Global Delivery', + globalDeliveryDesc: '`sendProgress` / `sendToolHints` are global switches and apply to all channels.', + openManager: 'Manage Channels', + defaultChannel: 'Default Channel', + customChannel: 'Custom Channel', + dashboardLocked: 'Dashboard is required and cannot be removed.', + enabled: 'Enabled', + saveChannel: 'Save', + addChannel: 'Add', + close: 'Close', + remove: 'Remove', + sendProgress: 'sendProgress', + sendToolHints: 'sendToolHints', + fieldPrimary: 'Primary Field', + fieldSecret: 'Secret Field', + fieldPort: 'Internal Port', + appId: 'App ID', + appSecret: 'App Secret', + clientId: 'Client ID', + clientSecret: 'Client Secret', + botToken: 'Bot Token', + appToken: 'App Token', + telegramToken: 'Telegram Bot Token', + proxy: 'Proxy (optional)', + replyToMessage: 'Reply to original message', + encryptKey: 'Encrypt Key (optional)', + verificationToken: 'Verification Token (optional)', + autoRefresh: 'Auto', + refreshHint: 'Refresh directory', +} as const; diff --git a/frontend/src/i18n/channels.zh-cn.ts b/frontend/src/i18n/channels.zh-cn.ts new file mode 100644 index 0000000..ecf654f --- /dev/null +++ b/frontend/src/i18n/channels.zh-cn.ts @@ -0,0 +1,33 @@ +export const channelsZhCn = { + wizardSectionTitle: '渠道配置', + wizardSectionDesc: '管理自定义渠道。', + globalDeliveryTitle: '全局消息投递', + globalDeliveryDesc: 'sendProgress / sendToolHints 为全局开关,对所有渠道统一生效。', + openManager: '管理渠道', + defaultChannel: '默认渠道', + customChannel: '自定义渠道', + dashboardLocked: 'Dashboard 为系统必选渠道,不能删除。', + enabled: '启用', + saveChannel: '保存', + addChannel: '新增', + close: '关闭', + remove: '删除', + sendProgress: 'sendProgress', + sendToolHints: 'sendToolHints', + fieldPrimary: '主字段', + fieldSecret: '密钥字段', + fieldPort: '内部端口', + appId: 'App ID', + appSecret: 'App Secret', + clientId: 'Client ID', + clientSecret: 'Client Secret', + botToken: 'Bot Token', + appToken: 'App Token', + telegramToken: 'Telegram Bot Token', + proxy: '代理(可选)', + replyToMessage: '回复时引用原消息', + encryptKey: 'Encrypt Key(可选)', + verificationToken: 'Verification Token(可选)', + autoRefresh: '自动', + refreshHint: '手动刷新目录', +} as const; diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts new file mode 100644 index 0000000..0c84fc5 --- /dev/null +++ b/frontend/src/i18n/dashboard.en.ts @@ -0,0 +1,136 @@ +export const dashboardEn = { + processing: 'Processing...', + executingWith: (action: string) => `Executing: ${action}`, + executing: 'Nanobot is executing tools...', + thinkingWith: (action: string) => `Thinking: ${action}`, + thinking: 'Nanobot is thinking...', + fileReadFail: 'Failed to read file.', + workspaceLoadFail: 'Failed to load workspace tree.', + channelSaved: 'Channel saved.', + channelSaveFail: 'Failed to save channel.', + channelAddFail: 'Failed to add channel.', + channelDeleteConfirm: (channelType: string) => `Delete channel ${channelType}?`, + channelDeleteFail: 'Failed to delete channel.', + stopFail: 'Stop failed. Check backend logs.', + startFail: 'Start failed. Check backend logs.', + backendDeliverFail: 'Backend failed to deliver command.', + sendFail: 'Send failed.', + uploadFile: 'Upload file', + uploadingFile: 'Uploading...', + uploadFail: 'File upload failed.', + attachmentMessage: '[attachment message]', + removeAttachment: 'Remove attachment', + sendFailMsg: (msg: string) => `Command delivery failed: ${msg}`, + providerRequired: 'Set provider/model/new API key before testing.', + connOk: (preview: string) => (preview ? `Connection passed, models: ${preview}` : 'Connection passed'), + connFail: (msg: string) => `Failed: ${msg}`, + configUpdated: 'Configuration updated.', + saveFail: 'Save failed.', + deleteBotConfirm: (id: string) => `Delete bot ${id}? Workspace will be removed.`, + deleteBotDone: 'Bot and workspace deleted.', + deleteFail: 'Delete failed.', + titleBots: 'Bots', + newBot: 'New Bot', + manageImages: 'Image Manager', + image: 'Image', + stop: 'Stop', + start: 'Start', + delete: 'Delete', + noConversation: 'No conversation yet. Send a command and bot replies will appear here.', + clearHistory: 'Clear history', + clearHistoryConfirm: (name: string) => `Clear dashboard conversation history for ${name}?`, + clearHistoryDone: 'Conversation history cleared.', + clearHistoryFail: 'Failed to clear conversation history.', + exportHistory: 'Export JSON', + exportHistoryFail: 'Failed to export conversation.', + you: 'You', + user: 'User', + inputPlaceholder: 'Type a command and press Enter (Shift+Enter for newline)', + disabledPlaceholder: 'Bot is not running. Command input is disabled.', + sending: 'Sending...', + send: 'Send', + botStarting: 'Bot is starting...', + botStopping: 'Bot is stopping...', + chatDisabled: 'Bot is stopped. Chat area is disabled.', + selectBot: 'Select a bot to inspect', + runtime: 'Runtime Status', + base: 'Base', + params: 'Params', + channels: 'Channels', + skills: 'Skills', + tools: 'Tools', + skillsPanel: 'Skills Panel', + skillsEmpty: 'No skills.', + addSkill: 'Add skill', + removeSkill: 'Remove skill', + uploadZipSkill: 'Upload ZIP Skill Pack', + zipOnlyHint: 'Only .zip files are supported', + invalidZipFile: 'Only .zip files are allowed', + envParams: 'Env Params', + envParamsDesc: 'Configure runtime env vars for this bot container.', + noEnvParams: 'No env vars configured.', + envKey: 'Key (e.g. BRAVE_API_KEY)', + envValue: 'Value', + showEnvValue: 'Show value', + hideEnvValue: 'Hide value', + addEnvParam: 'Add env var', + removeEnvParam: 'Remove env var', + envParamsSaved: 'Env params saved.', + envParamsSaveFail: 'Failed to save env params.', + envParamsHint: 'Restart bot to apply updated env vars.', + toolsLoadFail: 'Failed to load tool skills.', + toolsAddFail: 'Failed to add tool.', + toolsRemoveFail: 'Failed to remove tool.', + toolsRemoveConfirm: (name: string) => `Remove skill ${name}?`, + agent: 'Agent', + container: 'Container', + current: 'Current', + lastAction: 'Last action', + workspaceOutputs: 'Workspace', + autoRefresh: 'Auto refresh', + loadingDir: 'Loading directory...', + emptyDir: 'Current directory is empty.', + openingPreview: 'Opening file preview...', + workspaceHint: 'Click folder to enter; click .md/.json to preview.', + noPreviewFile: 'No previewable files in this directory.', + noTelemetry: 'No telemetry selected', + goUpTitle: 'Click to go up', + goUp: 'Go up', + openFolderTitle: 'Click to open folder', + folder: 'Folder', + previewTitle: 'Click to preview', + fileNotPreviewable: 'File type not previewable', + baseConfig: 'Base Configuration', + baseConfigSub: 'Only editable: name and LLM access settings', + botIdReadonly: 'Bot ID (Read-only)', + botName: 'Bot Name', + botNamePlaceholder: 'Bot name', + baseImageReadonly: 'Base Image (Read-only)', + modelName: 'Model Name', + modelNamePlaceholder: 'e.g. qwen-plus', + newApiKey: 'New API Key (optional)', + newApiKeyPlaceholder: 'Only updated when filled', + testing: 'Testing...', + testModelConnection: 'Test model connection', + cancel: 'Cancel', + save: 'Save', + modelParams: 'Model Parameters', + saveParams: 'Save Params', + agentFiles: 'Agent Files', + saveFiles: 'Save Files', + filePreview: 'File Preview', + fileTruncated: 'Large file: preview is truncated.', + download: 'Download', + close: 'Close', + cronViewer: 'Scheduled Jobs', + cronReload: 'Reload jobs', + cronLoading: 'Loading jobs...', + cronEmpty: 'No scheduled jobs.', + cronEnabled: 'Enabled', + cronDisabled: 'Disabled', + cronStop: 'Stop job', + cronDelete: 'Delete job', + cronStopFail: 'Failed to stop job.', + cronDeleteFail: 'Failed to delete job.', + cronDeleteConfirm: (id: string) => `Delete scheduled job ${id}?`, +}; diff --git a/frontend/src/i18n/dashboard.zh-cn.ts b/frontend/src/i18n/dashboard.zh-cn.ts new file mode 100644 index 0000000..9c27231 --- /dev/null +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -0,0 +1,136 @@ +export const dashboardZhCn = { + processing: '处理中...', + executingWith: (action: string) => `执行中:${action}`, + executing: 'Nanobot 正在执行工具...', + thinkingWith: (action: string) => `思考中:${action}`, + thinking: 'Nanobot 正在思考中...', + fileReadFail: '文件读取失败。', + workspaceLoadFail: '无法读取 workspace 目录。', + channelSaved: '渠道配置已保存。', + channelSaveFail: '渠道保存失败。', + channelAddFail: '新增渠道失败。', + channelDeleteConfirm: (channelType: string) => `确认删除渠道 ${channelType}?`, + channelDeleteFail: '删除渠道失败。', + stopFail: '停止失败,请查看后端日志。', + startFail: '启动失败,请查看后端日志。', + backendDeliverFail: '后端未成功投递指令。', + sendFail: '发送失败。', + uploadFile: '上传文件', + uploadingFile: '上传中...', + uploadFail: '文件上传失败。', + attachmentMessage: '[附件消息]', + removeAttachment: '移除附件', + sendFailMsg: (msg: string) => `指令发送失败:${msg}`, + providerRequired: '请填写 Provider、模型和新 API Key 后再测试。', + connOk: (preview: string) => (preview ? `连接成功,模型: ${preview}` : '连接成功'), + connFail: (msg: string) => `连接失败: ${msg}`, + configUpdated: '配置已更新。', + saveFail: '保存失败。', + deleteBotConfirm: (id: string) => `确认删除 Bot ${id}?将删除对应 workspace。`, + deleteBotDone: 'Bot 与 workspace 已删除。', + deleteFail: '删除失败。', + titleBots: 'Bot 列表', + newBot: '新建机器人', + manageImages: '镜像管理', + image: '镜像', + stop: '停止', + start: '启动', + delete: '删除', + noConversation: '暂无对话消息。请先发送指令,Bot 回复会在这里按标准会话格式展示。', + clearHistory: '清空对话', + clearHistoryConfirm: (name: string) => `确认清空 ${name} 的面板对话历史?`, + clearHistoryDone: '对话历史已清空。', + clearHistoryFail: '清空对话历史失败。', + exportHistory: '导出对话', + exportHistoryFail: '导出对话失败。', + you: '你', + user: '用户', + inputPlaceholder: '输入指令后回车发送(Shift+Enter 换行)', + disabledPlaceholder: 'Bot 未运行,无法发送指令。', + sending: '发送中...', + send: '发送', + botStarting: 'Bot 正在启动中...', + botStopping: 'Bot 正在停止中...', + chatDisabled: 'Bot 已停止,对话区已禁用。', + selectBot: '请选择 Bot 查看详情', + runtime: '运行状态', + base: '基础', + params: '参数', + channels: '渠道', + skills: '技能', + tools: '工具', + skillsPanel: '技能面板', + skillsEmpty: '暂无技能。', + addSkill: '新增技能', + removeSkill: '移除技能', + uploadZipSkill: '上传 ZIP 技能包', + zipOnlyHint: '仅支持 .zip 文件', + invalidZipFile: '只能上传 .zip 文件', + envParams: '环境变量', + envParamsDesc: '配置该 Bot 的运行时环境变量(容器级)。', + noEnvParams: '暂无环境变量。', + envKey: '变量名(如 BRAVE_API_KEY)', + envValue: '变量值', + showEnvValue: '显示变量值', + hideEnvValue: '隐藏变量值', + addEnvParam: '新增环境变量', + removeEnvParam: '移除环境变量', + envParamsSaved: '环境变量已保存。', + envParamsSaveFail: '环境变量保存失败。', + envParamsHint: '修改后需重启 Bot 才会生效。', + toolsLoadFail: '读取工具技能失败。', + toolsAddFail: '新增工具失败。', + toolsRemoveFail: '移除工具失败。', + toolsRemoveConfirm: (name: string) => `确认移除技能 ${name}?`, + agent: '代理', + container: '容器状态', + current: '当前状态', + lastAction: '最后动作', + workspaceOutputs: '工作区', + autoRefresh: '自动刷新', + loadingDir: '读取目录中...', + emptyDir: '当前目录为空。', + openingPreview: '正在打开文件预览...', + workspaceHint: '点击目录进入;点击 .md/.json 文件弹出预览。', + noPreviewFile: '本目录没有可预览文件。', + noTelemetry: '暂无可展示信息', + goUpTitle: '点击返回上级目录', + goUp: '返回上级', + openFolderTitle: '点击进入目录', + folder: '目录', + previewTitle: '点击打开预览', + fileNotPreviewable: '该文件类型不可预览', + baseConfig: '基础配置', + baseConfigSub: '仅允许修改名称和大模型接入信息', + botIdReadonly: 'Bot ID(只读)', + botName: 'Bot 名称', + botNamePlaceholder: '输入 Bot 名称', + baseImageReadonly: '基础镜像(只读)', + modelName: '模型名称', + modelNamePlaceholder: '如 qwen-plus', + newApiKey: '新的 API Key(留空不更新)', + newApiKeyPlaceholder: '输入新 Key 才会更新', + testing: '测试中...', + testModelConnection: '测试模型连接', + cancel: '取消', + save: '保存', + modelParams: '模型参数设置', + saveParams: '保存参数', + agentFiles: '代理文件配置', + saveFiles: '保存文件', + filePreview: '文件预览', + fileTruncated: '文件较大,当前内容为截断预览。', + download: '下载', + close: '关闭', + cronViewer: '定时任务', + cronReload: '刷新任务', + cronLoading: '读取任务中...', + cronEmpty: '暂无定时任务。', + cronEnabled: '启用', + cronDisabled: '已停用', + cronStop: '停止任务', + cronDelete: '删除任务', + cronStopFail: '停止任务失败。', + cronDeleteFail: '删除任务失败。', + cronDeleteConfirm: (id: string) => `确认删除任务 ${id}?`, +}; diff --git a/frontend/src/i18n/image-factory.en.ts b/frontend/src/i18n/image-factory.en.ts new file mode 100644 index 0000000..df8c0cb --- /dev/null +++ b/frontend/src/i18n/image-factory.en.ts @@ -0,0 +1,23 @@ +export const imageFactoryEn = { + fetchFail: 'Failed to fetch images. Check backend.', + registerFail: 'Image registration failed', + deleteFail: 'Delete failed', + deleteConfirm: (tag: string) => `Delete image registration?\n${tag}\n\nThis only removes DB registration, not local Docker image.`, + registeredTitle: 'Registered Base Images', + registeredDesc: 'Wizard can only use READY images from DB registry.', + refreshing: 'Refreshing...', + refresh: 'Refresh', + partialLoadFail: (detail: string) => `Partial load failure: ${detail}`, + registeredCount: 'Registered', + abnormal: 'Abnormal', + action: 'Action', + deleting: 'Deleting...', + deleteRegistry: 'Delete', + noRegistered: 'No registered image in DB.', + dockerTitle: 'Docker Local Images', + dockerDesc: 'System no longer scans engines. Register from docker images only.', + dockerTip: 'Build manually then register: docker build -f Dashboard.Dockerfile.manual -t nanobot-base:v0.1.4 .', + update: 'Update', + register: 'Register', + noDocker: 'No local nanobot-base:* image found.', +}; diff --git a/frontend/src/i18n/image-factory.zh-cn.ts b/frontend/src/i18n/image-factory.zh-cn.ts new file mode 100644 index 0000000..257a9da --- /dev/null +++ b/frontend/src/i18n/image-factory.zh-cn.ts @@ -0,0 +1,23 @@ +export const imageFactoryZhCn = { + fetchFail: '获取镜像列表失败,请检查后端。', + registerFail: '镜像注册失败', + deleteFail: '删除失败', + deleteConfirm: (tag: string) => `确认删除该镜像登记?\n${tag}\n\n仅删除数据库登记,不会删除 Docker 本地镜像。`, + registeredTitle: '已有基础镜像(数据库登记)', + registeredDesc: '创建向导只允许使用 READY 的已登记镜像。', + refreshing: '刷新中...', + refresh: '刷新', + partialLoadFail: (detail: string) => `部分数据加载失败: ${detail}`, + registeredCount: '登记总数', + abnormal: '异常状态', + action: '操作', + deleting: '删除中...', + deleteRegistry: '删除登记', + noRegistered: '数据库暂无登记镜像。', + dockerTitle: '可登记镜像(Docker 本地)', + dockerDesc: '系统不再扫描 engines,仅从 docker images 获取并手工登记。', + dockerTip: '建议手工构建后再登记: docker build -f Dashboard.Dockerfile.manual -t nanobot-base:v0.1.4 .', + update: '更新登记', + register: '加入数据库', + noDocker: '本地没有 nanobot-base:* 镜像,请先手工构建。', +}; diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts new file mode 100644 index 0000000..e5027b4 --- /dev/null +++ b/frontend/src/i18n/index.ts @@ -0,0 +1,11 @@ +import type { AppLocale } from '../store/appStore'; + +export type UiLocale = 'zh-cn' | 'en'; + +export function toUiLocale(locale: AppLocale): UiLocale { + return locale === 'zh' ? 'zh-cn' : 'en'; +} + +export function pickLocale(locale: AppLocale, packs: Record): T { + return packs[toUiLocale(locale)]; +} diff --git a/frontend/src/i18n/legacy-deck.en.ts b/frontend/src/i18n/legacy-deck.en.ts new file mode 100644 index 0000000..2927248 --- /dev/null +++ b/frontend/src/i18n/legacy-deck.en.ts @@ -0,0 +1,11 @@ +export const legacyDeckEn = { + operationsDeck: 'OPERATIONS DECK', + sandbox: 'Neural Network Sandbox', + activeNodes: 'ACTIVE NODES', + globalEvents: 'Global Events', + listening: 'listening for commands', + standby: 'All units in standby mode. Waiting for deployment...', + deckTitle: 'Digital Employee Deck', + deckSub: 'Realtime bot execution and data flow visibility', + idle: 'IDLE', +}; diff --git a/frontend/src/i18n/legacy-deck.zh-cn.ts b/frontend/src/i18n/legacy-deck.zh-cn.ts new file mode 100644 index 0000000..ed6c180 --- /dev/null +++ b/frontend/src/i18n/legacy-deck.zh-cn.ts @@ -0,0 +1,11 @@ +export const legacyDeckZhCn = { + operationsDeck: '运维控制台', + sandbox: '神经网络沙盒', + activeNodes: '活跃节点', + globalEvents: '全局事件', + listening: '等待指令', + standby: '所有单元待命中,等待部署...', + deckTitle: '数字员工工位', + deckSub: '实时观察机器人执行状态与数据流转', + idle: 'IDLE', +}; diff --git a/frontend/src/i18n/management.en.ts b/frontend/src/i18n/management.en.ts new file mode 100644 index 0000000..bd1a125 --- /dev/null +++ b/frontend/src/i18n/management.en.ts @@ -0,0 +1,54 @@ +export const managementEn = { + opFail: 'Operation failed. Check Docker.', + botInstances: 'Bot Instances', + newBot: 'New Bot', + detailsTitle: 'Instance configuration and runtime details', + kernel: 'Kernel', + model: 'Model', + status: 'Status', + consoleOutput: 'Console Output', + selectHint: 'Select an instance to view detailed diagnostics.', + idle: 'IDLE', + standingBy: 'Standing by...', + create: { + createFail: 'Create failed. Check backend service and duplicate ID.', + title: 'Create New Digital Worker', + idLabel: 'Unique ID', + idPlaceholder: 'e.g. research_bot_01', + nameLabel: 'Worker Name', + namePlaceholder: 'e.g. Market Research Expert', + imageLabel: 'Kernel Image (Docker Image)', + noImage: 'No available kernel image found', + imageHint: 'Only READY images can be used to create a bot', + providerLabel: 'Model Provider', + modelLabel: 'Model', + modelPlaceholder: 'gpt-4o / deepseek-chat', + soulLabel: 'Persona Prompt (SOUL.md)', + cancel: 'Cancel', + submit: 'Create Bot', + source: 'Source', + pypi: 'PyPI', + systemPrompt: 'You are a professional digital worker assistant.', + }, + kernelModal: { + fetchFailed: 'Fetch failed', + removeConfirm: (tag: string) => `Remove image registration?\n${tag}`, + removeFail: 'Remove failed', + needVersionAndEngine: 'Please enter version and select source directory', + buildDispatched: 'Build task submitted. Watch status updates on the right list.', + buildDispatchFail: 'Failed to submit build task', + title: 'Nanobot Kernel Manager (Image Factory)', + buildSection: 'Build New Kernel Image', + selectEngine: '1. Select local source directory (engines/)', + noEngine: 'No source detected (please git clone into engines/)', + buildHint: 'Backend scans this directory and runs `pip install .` for image build', + defineVersion: '2. Define version tag', + versionPlaceholder: 'e.g. 0.1.4-custom', + startBuild: 'Start Build', + readySection: 'Ready Image Versions', + source: 'Source', + pypi: 'PyPI', + removeRecord: 'Remove record', + footerHint: 'Note: image build runs asynchronously in host Docker. Source must include `pyproject.toml` or `setup.py`.', + }, +}; diff --git a/frontend/src/i18n/management.zh-cn.ts b/frontend/src/i18n/management.zh-cn.ts new file mode 100644 index 0000000..f7b7be3 --- /dev/null +++ b/frontend/src/i18n/management.zh-cn.ts @@ -0,0 +1,54 @@ +export const managementZhCn = { + opFail: '操作失败,请检查 Docker', + botInstances: 'Bot 实例', + newBot: '新建 Bot', + detailsTitle: '实例配置与运行时信息', + kernel: '内核', + model: '模型', + status: '状态', + consoleOutput: '控制台输出', + selectHint: '请选择实例查看详细诊断。', + idle: 'IDLE', + standingBy: '待命中...', + create: { + createFail: '创建失败,请检查后端服务是否正常及 ID 是否重复', + title: '孵化新的数字员工', + idLabel: '唯一标识 (ID)', + idPlaceholder: '例如: research_bot_01', + nameLabel: '员工姓名', + namePlaceholder: '例如: 市场调研专家', + imageLabel: '选择内核版本 (Docker Image)', + noImage: '未发现可用内核镜像', + imageHint: '只有状态为 READY 的镜像才能用于孵化机器人', + providerLabel: '模型供应商', + modelLabel: '模型型号', + modelPlaceholder: 'gpt-4o / deepseek-chat', + soulLabel: '人设提示词 (SOUL.md)', + cancel: '取消', + submit: '确认孵化', + source: '来源', + pypi: 'PyPI', + systemPrompt: '你是一个专业的数字员工助手。', + }, + kernelModal: { + fetchFailed: '加载失败', + removeConfirm: (tag: string) => `确定要移除镜像登记吗?\n${tag}`, + removeFail: '移除失败', + needVersionAndEngine: '请填写版本号并选择源码目录', + buildDispatched: '构建指令已下发,请在右侧列表关注状态更新', + buildDispatchFail: '构建指令发送失败', + title: 'Nanobot 内核管理 (Image Factory)', + buildSection: '构建新内核镜像', + selectEngine: '1. 选择本地源码目录 (engines/)', + noEngine: '未检测到源码 (请先 git clone 到 engines/)', + buildHint: '后端将扫描该目录并执行 `pip install .` 构建镜像', + defineVersion: '2. 定义版本号 (Version Tag)', + versionPlaceholder: '例如: 0.1.4-custom', + startBuild: '开始构建镜像', + readySection: '已就绪的镜像版本', + source: '来源', + pypi: 'PyPI', + removeRecord: '移除记录', + footerHint: '提示:镜像构建在宿主机 Docker 中异步执行。源码目录需包含 `pyproject.toml` 或 `setup.py`。', + }, +}; diff --git a/frontend/src/i18n/wizard.en.ts b/frontend/src/i18n/wizard.en.ts new file mode 100644 index 0000000..def1ef2 --- /dev/null +++ b/frontend/src/i18n/wizard.en.ts @@ -0,0 +1,58 @@ +export const wizardEn = { + title: 'Guided Bot Creation', + sub: 'Select image first, then configure model and agent files.', + s1: '1. Select READY image', + s2: '2. Model and params', + s3: '3. Agent files', + s4: '4. Confirm', + test: 'Test model connection', + testing: 'Testing...', + next: 'Next', + prev: 'Back', + finish: 'Create bot', + noReadyImage: 'No READY images. Register image first.', + requiredBase: 'Please complete required fields.', + providerRequired: 'Please set provider/model/API key first.', + connFailed: (msg: string) => `Failed: ${msg}`, + connOk: (preview: string) => (preview ? `Connection passed, models: ${preview}` : 'Connection passed'), + created: 'Bot created.', + createFailed: 'Create failed, check backend logs', + loading: 'Loading...', + loadImages: 'Load images', + noReady: 'No READY image.', + baseInfo: 'Base Info', + botIdPlaceholder: 'Bot ID', + botName: 'Bot Name', + modelAccess: 'Model Access', + modelNamePlaceholder: 'Model name', + modelParams: 'Model Parameters', + skills: 'Skills', + skillsConfig: 'Skills Configuration', + skillsDesc: 'Select skills to install for this bot.', + openSkillsManager: 'Open skills panel', + noSkillsSelected: 'No skills selected', + skillsSectionTitle: 'Skills Manager', + skillsEmpty: 'No skills available.', + addSkill: 'Add skill', + removeSkill: 'Remove skill', + tools: 'Env Params', + toolsConfig: 'Env Params', + toolsDesc: 'Configure runtime env vars (effective after bot restart).', + openToolsManager: 'Open env params panel', + toolsSectionTitle: 'Environment Variables', + envParamsDesc: 'Env vars are injected into this bot container for tools/skills.', + noEnvParams: 'No env vars configured.', + envKey: 'Key (e.g. BRAVE_API_KEY)', + envValue: 'Value', + showEnvValue: 'Show value', + hideEnvValue: 'Hide value', + addEnvParam: 'Add env var', + removeEnvParam: 'Remove env var', + tokenRange: 'Range: 256 - 32768', + image: 'Image', + name: 'Name', + model: 'Model', + channels: 'Channels', + autoStart: 'Auto start after creation', + creating: 'Creating...', +}; diff --git a/frontend/src/i18n/wizard.zh-cn.ts b/frontend/src/i18n/wizard.zh-cn.ts new file mode 100644 index 0000000..d81d307 --- /dev/null +++ b/frontend/src/i18n/wizard.zh-cn.ts @@ -0,0 +1,58 @@ +export const wizardZhCn = { + title: '引导式 Bot 创建', + sub: '先选镜像,再配置模型与代理文件。', + s1: '1. 选择 READY 镜像', + s2: '2. 模型与参数', + s3: '3. 代理文件配置', + s4: '4. 确认创建', + test: '测试模型连接', + testing: '测试中...', + next: '下一步', + prev: '上一步', + finish: '完成创建', + noReadyImage: '没有 READY 镜像,请先在镜像工厂登记。', + requiredBase: '请完整填写基础信息。', + providerRequired: '请先填写 provider、模型和 API Key。', + connFailed: (msg: string) => `连接失败: ${msg}`, + connOk: (preview: string) => (preview ? `连接成功,返回模型: ${preview}` : '连接成功'), + created: 'Bot 创建成功。', + createFailed: '创建失败,请检查后端日志', + loading: '加载中...', + loadImages: '加载镜像列表', + noReady: '暂无 READY 镜像。', + baseInfo: '基础信息', + botIdPlaceholder: 'Bot ID(如 analyst_bot_01)', + botName: 'Bot 名称', + modelAccess: '模型接入', + modelNamePlaceholder: '模型名(如 qwen-plus)', + modelParams: '模型参数', + skills: '技能', + skillsConfig: '技能配置', + skillsDesc: '选择该 Bot 需要安装的技能包。', + openSkillsManager: '打开技能面板', + noSkillsSelected: '未选择技能', + skillsSectionTitle: '技能管理', + skillsEmpty: '暂无可用技能。', + addSkill: '新增技能', + removeSkill: '移除技能', + tools: '环境变量', + toolsConfig: '环境变量配置', + toolsDesc: '配置运行时环境变量(容器重启后生效)。', + openToolsManager: '打开环境变量面板', + toolsSectionTitle: '环境变量', + envParamsDesc: '环境变量将注入该 Bot 容器,供 tools/skills 统一读取。', + noEnvParams: '暂无环境变量。', + envKey: '变量名(如 BRAVE_API_KEY)', + envValue: '变量值', + showEnvValue: '显示变量值', + hideEnvValue: '隐藏变量值', + addEnvParam: '新增环境变量', + removeEnvParam: '移除环境变量', + tokenRange: '范围: 256 - 32768', + image: '镜像', + name: '名称', + model: '模型', + channels: '渠道', + autoStart: '创建后自动启动', + creating: '创建中...', +}; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..d3147e6 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,7 @@ +html, +body, +#root { + margin: 0; + width: 100%; + min-height: 100vh; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..04229a4 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' +import { LucentPromptProvider } from './components/lucent/LucentPromptProvider.tsx' + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css new file mode 100644 index 0000000..d348b6a --- /dev/null +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -0,0 +1,1573 @@ +.ops-bot-list { + min-width: 0; +} + +.ops-compact-hidden { + display: none !important; +} + +.ops-compact-fab-switch { + position: fixed; + right: 14px; + bottom: 14px; + width: 42px; + height: 42px; + border-radius: 999px; + border: 1px solid color-mix(in oklab, var(--brand) 58%, var(--line) 42%); + background: color-mix(in oklab, var(--panel) 70%, var(--brand-soft) 30%); + color: var(--icon); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 10px 24px rgba(9, 15, 28, 0.35); + z-index: 85; + cursor: pointer; +} + +.ops-list-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.ops-bot-card { + position: relative; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel-soft); + padding: 10px 10px 10px 14px; + margin-bottom: 10px; + cursor: pointer; + transition: border-color 0.2s ease, transform 0.2s ease; +} + +.ops-bot-card:hover { + border-color: var(--brand); +} + +.ops-bot-card.is-active { + border-color: var(--brand); + box-shadow: inset 0 0 0 1px var(--brand); +} + +.ops-bot-top { + align-items: flex-start; +} + +.ops-bot-name { + font-size: 16px; + font-weight: 800; + color: var(--title); +} + +.ops-bot-id, +.ops-bot-meta { + margin-top: 2px; + color: var(--subtitle); + font-size: 12px; + font-weight: 600; +} + +.ops-bot-actions { + margin-top: 10px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.ops-bot-strip { + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 4px; + border-radius: 999px; + background: #d14b4b; +} + +.ops-bot-strip.is-running { + background: #2ca476; +} + +.ops-bot-strip.is-stopped { + background: #d14b4b; +} + +.ops-bot-icon-btn { + width: 34px; + height: 34px; + padding: 0; + border-radius: 9px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.ops-control-pending { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.ops-control-dots { + display: inline-flex; + align-items: center; + gap: 3px; +} + +.ops-control-dots i { + width: 4px; + height: 4px; + border-radius: 999px; + display: block; + background: currentColor; + opacity: 0.35; + animation: ops-control-dot 1.2s infinite ease-in-out; +} + +.ops-control-dots i:nth-child(2) { + animation-delay: 0.2s; +} + +.ops-control-dots i:nth-child(3) { + animation-delay: 0.4s; +} + +.btn-sm { + padding: 8px 10px; + font-size: 12px; +} + +.ops-chat-panel { + min-width: 0; + padding: 12px; +} + +.ops-chat-shell { + display: grid; + grid-template-rows: 1fr; + gap: 0; + height: 100%; + min-height: 0; +} + +.ops-chat-head { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel-soft); + padding: 10px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.ops-chat-title { + margin: 0; + font-size: 18px; + font-weight: 800; + color: var(--title); +} + +.ops-chat-subtitle { + margin: 4px 0 0; + color: var(--subtitle); + font-size: 12px; + font-weight: 600; +} + +.ops-status-group { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.ops-status-pill { + border: 1px solid var(--line); + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + font-weight: 800; + color: var(--text); + background: var(--panel); +} + +.ops-status-pill.running { + border-color: #2ca476; + background: rgba(44, 164, 118, 0.16); +} + +.ops-status-pill.stopped, +.ops-status-pill.exited { + border-color: #d28686; + background: rgba(209, 75, 75, 0.16); +} + +.ops-status-pill.error { + border-color: #d14b4b; + background: rgba(209, 75, 75, 0.2); +} + +.ops-status-pill.idle { + border-color: #4b79d4; + background: rgba(47, 105, 226, 0.16); +} + +.ops-chat-frame { + position: relative; + min-height: 0; + display: grid; + grid-template-rows: 1fr auto; + gap: 10px; +} + +.ops-chat-frame.is-disabled .ops-chat-scroll, +.ops-chat-frame.is-disabled .ops-composer { + filter: grayscale(0.2) opacity(0.75); +} + +.ops-chat-disabled-mask { + position: absolute; + inset: 0; + border-radius: 12px; + background: color-mix(in oklab, var(--panel) 60%, transparent); + display: flex; + align-items: center; + justify-content: center; + z-index: 3; +} + +.ops-chat-disabled-card { + border: 1px solid color-mix(in oklab, var(--line) 78%, var(--brand) 22%); + border-radius: 10px; + background: color-mix(in oklab, var(--panel-soft) 82%, var(--panel) 18%); + color: var(--text); + font-size: 13px; + font-weight: 700; + padding: 10px 12px; +} + +.ops-midstate-board { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel-soft); + padding: 10px; + display: grid; + gap: 8px; +} + +.ops-midstate-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.ops-midstate-feed { + display: grid; + gap: 6px; + max-height: 180px; + overflow: auto; +} + +.ops-midstate-item { + border: 1px solid var(--line); + border-radius: 10px; + background: var(--panel); + padding: 6px 8px; + display: grid; + grid-template-columns: 86px 1fr auto; + gap: 8px; + align-items: center; +} + +.ops-midstate-text { + font-size: 12px; + line-height: 1.35; + color: var(--text); +} + +.ops-chat-scroll { + min-height: 440px; + max-height: 68vh; + overflow: auto; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--panel-soft); + padding: 14px; +} + +.ops-chat-row { + display: flex; + width: 100%; + margin-bottom: 12px; +} + +.ops-chat-item { + display: flex; + align-items: flex-end; + gap: 10px; + width: 100%; + min-width: 0; +} + +.ops-chat-item.is-user { + justify-content: flex-end; +} + +.ops-chat-item.is-assistant { + justify-content: flex-start; +} + +.ops-chat-row.is-user { + justify-content: flex-end; +} + +.ops-chat-row.is-assistant { + justify-content: flex-start; +} + +.ops-chat-bubble { + width: auto; + min-width: 0; + flex: 0 1 auto; + max-width: min(860px, calc(100% - 52px)); + border-radius: 14px; + border: 1px solid var(--line); + padding: 10px 12px; +} + +.ops-chat-bubble.assistant { + border-color: #3661aa; + background: color-mix(in oklab, var(--panel-soft) 82%, var(--panel) 18%); +} + +.ops-chat-bubble.assistant.progress { + border-style: dashed; + border-color: color-mix(in oklab, var(--brand) 55%, var(--line) 45%); + background: color-mix(in oklab, var(--brand-soft) 35%, var(--panel-soft) 65%); +} + +.ops-chat-bubble.user { + border-color: #2f8f7f; + background: color-mix(in oklab, #d9fff0 36%, var(--panel-soft) 64%); +} + +.ops-chat-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 11px; + font-weight: 700; + margin-bottom: 6px; + color: var(--muted); +} + +.ops-chat-text { + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; + line-height: 1.58; + font-size: 14px; + color: var(--text); +} + +.ops-chat-text > *:first-child { + margin-top: 0; +} + +.ops-chat-text > *:last-child { + margin-bottom: 0; +} + +.ops-chat-text p, +.ops-chat-text ul, +.ops-chat-text ol { + margin: 8px 0; +} + +.ops-chat-text pre { + margin: 8px 0; + overflow: auto; + border-radius: 10px; + border: 1px solid color-mix(in oklab, var(--line) 75%, transparent); + background: color-mix(in oklab, var(--panel) 78%, #000 22%); + padding: 10px 12px; +} + +.ops-chat-text code { + font-family: 'SF Mono', Menlo, Consolas, monospace; + font-size: 12px; + word-break: break-word; +} + +.ops-chat-attachments { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ops-attach-link { + display: inline-flex; + align-items: center; + max-width: 100%; + padding: 4px 8px; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 12px; + text-decoration: none; + color: var(--text); + background: color-mix(in oklab, var(--panel) 78%, transparent); +} + +.ops-attach-link:hover { + border-color: color-mix(in oklab, var(--brand) 56%, var(--line) 44%); +} + +.ops-avatar { + width: 34px; + height: 34px; + border-radius: 999px; + border: 1px solid var(--line); + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 34px; + overflow: hidden; +} + +.ops-avatar.bot { + background: #102d63; + border-color: #4e70ad; +} + +.ops-avatar.bot img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.ops-avatar.user { + background: #1f4f8a; + border-color: #6d98cf; + color: #e9f2ff; +} + +.ops-chat-empty { + border: 1px dashed var(--line); + border-radius: 12px; + padding: 12px; + color: var(--muted); + font-size: 14px; +} + +.ops-thinking-bubble { + border: 1px solid color-mix(in oklab, var(--brand) 50%, var(--line) 50%); + background: color-mix(in oklab, var(--brand-soft) 40%, var(--panel-soft) 60%); + border-radius: 14px; + padding: 10px 12px; + min-width: 190px; +} + +.ops-thinking-cloud { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.ops-thinking-cloud .dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: color-mix(in oklab, var(--brand) 75%, #fff 25%); + animation: ops-thinking-pulse 1.4s infinite ease-in-out; +} + +.ops-thinking-cloud .dot:nth-child(2) { + animation-delay: 0.2s; +} + +.ops-thinking-cloud .dot:nth-child(3) { + animation-delay: 0.4s; +} + +.ops-thinking-text { + margin-top: 8px; + color: var(--subtitle); + font-size: 12px; + font-weight: 700; +} + +@keyframes ops-thinking-pulse { + 0%, 80%, 100% { + transform: translateY(0); + opacity: 0.45; + } + 40% { + transform: translateY(-4px); + opacity: 1; + } +} + +@keyframes ops-control-dot { + 0%, 80%, 100% { + transform: translateY(0); + opacity: 0.35; + } + 40% { + transform: translateY(-2px); + opacity: 1; + } +} + +.ops-composer { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel-soft); + padding: 10px; + display: grid; + grid-template-columns: 1fr auto auto; + gap: 10px; + align-items: flex-end; +} + +.ops-composer-input { + min-height: 78px; + resize: none; + font-size: 14px; + line-height: 1.45; +} + +.ops-pending-files { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.ops-pending-chip { + display: inline-flex; + align-items: center; + gap: 6px; + max-width: 100%; + border: 1px solid var(--line); + border-radius: 999px; + padding: 4px 8px; + font-size: 12px; + color: var(--text); + background: color-mix(in oklab, var(--panel-soft) 78%, transparent); +} + +.ops-chip-remove { + width: 18px; + height: 18px; + min-width: 18px; + min-height: 18px; + border-radius: 999px; + color: var(--muted); +} + +.ops-runtime-panel { + min-width: 0; + min-height: 0; +} + +.ops-runtime-shell { + display: grid; + grid-template-rows: auto 1fr; + gap: 10px; + min-height: 0; + height: 100%; +} + +.ops-runtime-head { + flex-wrap: wrap; + align-items: center; +} + +.ops-panel-tools { + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + flex-wrap: wrap; +} + +.ops-more-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 190px; + z-index: 30; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel); + box-shadow: 0 14px 32px rgba(9, 16, 31, 0.28); + padding: 6px; + display: grid; + gap: 4px; +} + +.ops-more-item { + width: 100%; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--text); + padding: 8px 10px; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + text-align: left; +} + +.ops-more-item:hover { + background: color-mix(in oklab, var(--brand-soft) 36%, var(--panel-soft) 64%); + border-color: color-mix(in oklab, var(--brand) 48%, var(--line) 52%); +} + +.ops-more-item.danger { + color: #d76666; +} + +.ops-more-item.danger:hover { + background: rgba(215, 102, 102, 0.14); + border-color: rgba(215, 102, 102, 0.34); +} + +.ops-runtime-scroll { + min-height: 0; + overflow: auto; + display: grid; + gap: 10px; + align-content: start; + padding-right: 2px; +} + +.ops-runtime-card { + display: grid; + gap: 8px; +} + +.ops-runtime-state-card { + min-height: 0; +} + +.ops-runtime-state-card:not(.is-visual) { + min-height: 210px; + align-content: start; +} + +.ops-runtime-state-card.is-visual { + min-height: 210px; + padding: 0; + gap: 0; + overflow: hidden; +} + +.ops-runtime-state-card.is-visual .ops-state-stage { + height: 100%; + min-height: 100%; + border-radius: inherit; +} + +.ops-runtime-state-card.is-visual .ops-state-face { + width: 210px; + height: 140px; + gap: 34px; +} + +.ops-runtime-state-card.is-visual .ops-state-eye { + width: 30px; + height: 30px; +} + +.ops-runtime-state-card.is-visual .ops-state-eye::after { + width: 10px; + height: 10px; +} + +.ops-runtime-state-card.is-visual .ops-state-idle .ops-state-eye { + width: 36px; + height: 6px; +} + +.ops-runtime-state-card.is-visual .ops-state-caption { + bottom: 14px; + font-size: 12px; +} + +.ops-runtime-state-card.is-visual .ops-state-float { + padding: 6px; + right: 22%; + top: 24%; +} + +.ops-runtime-mode-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.ops-runtime-mode-toggle { + display: inline-flex; + gap: 6px; +} + +.ops-runtime-mode-toggle .btn.active { + border-color: color-mix(in oklab, var(--brand) 70%, var(--line) 30%); + background: color-mix(in oklab, var(--brand-soft) 58%, var(--panel-soft) 42%); +} + +.ops-state-stage { + position: relative; + height: 160px; + border: 0; + border-radius: 10px; + display: grid; + place-items: center; + background: + radial-gradient(circle at 20% 25%, rgba(95, 136, 210, 0.12), transparent 40%), + radial-gradient(circle at 82% 70%, rgba(53, 94, 176, 0.1), transparent 40%), + color-mix(in oklab, var(--panel-soft) 72%, var(--panel) 28%); + overflow: hidden; +} + +.ops-state-model { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + max-width: calc(100% - 28px); + border: 1px solid rgba(148, 179, 235, 0.55); + border-radius: 999px; + background: rgba(11, 24, 48, 0.58); + color: #dce9ff; + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ops-state-face { + width: 148px; + height: 96px; + border-radius: 999px; + border: 1px solid rgba(148, 179, 235, 0.55); + background: rgba(19, 37, 74, 0.55); + box-shadow: + 0 8px 30px rgba(10, 22, 44, 0.35), + inset 0 0 0 1px rgba(156, 185, 241, 0.18); + display: flex; + align-items: center; + justify-content: center; + gap: 26px; +} + +.ops-state-eye { + width: 22px; + height: 22px; + position: relative; + border-radius: 999px; + border: 3px solid #d8e7ff; + background: rgba(232, 242, 255, 0.95); + box-shadow: 0 0 0 1px rgba(145, 178, 234, 0.35); +} + +.ops-state-eye::after { + content: ""; + position: absolute; + width: 8px; + height: 8px; + border-radius: 999px; + background: #1b3569; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.ops-state-eye::before { + content: ""; + display: none; +} + +.ops-state-caption { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + font-size: 11px; + letter-spacing: 0.08em; + color: color-mix(in oklab, var(--text) 70%, var(--muted) 30%); +} + +.ops-state-float { + position: absolute; + color: #f2f7ff; + border: 1px solid rgba(163, 187, 226, 0.65); + background: rgba(17, 36, 71, 0.72); + border-radius: 999px; + padding: 4px; + right: 24%; + top: 26%; +} + +.ops-state-float.state-thinking { + animation: ops-state-orbit 1.8s linear infinite; +} + +.ops-state-float.state-tool { + animation: ops-tool-bob 0.7s ease-in-out infinite; +} + +.ops-state-float.state-success { + color: #62e2ad; + border-color: rgba(98, 226, 173, 0.6); +} + +.ops-state-float.state-error { + color: #ff9b9b; + border-color: rgba(255, 155, 155, 0.6); +} + +.ops-state-idle .ops-state-eye { + width: 28px; + height: 4px; + border: 0; + border-radius: 999px; + background: #d8e7ff; + box-shadow: none; +} + +.ops-state-idle .ops-state-eye::after { + display: none; +} + +.ops-state-thinking .ops-state-face { + animation: ops-state-breathe 1.25s ease-in-out infinite; +} + +.ops-state-thinking .ops-state-eye::after { + animation: ops-eye-scan 1.3s ease-in-out infinite; +} + +.ops-state-tool_call .ops-state-face { + animation: ops-state-breathe 0.8s ease-in-out infinite; +} + +.ops-state-tool_call .ops-state-eye::after { + animation: ops-eye-scan-fast 0.7s ease-in-out infinite; +} + +.ops-state-success .ops-state-face { + box-shadow: + 0 8px 30px rgba(11, 48, 37, 0.3), + inset 0 0 0 1px rgba(112, 225, 172, 0.35); +} + +.ops-state-success .ops-state-eye { + width: 22px; + height: 12px; + border: 0; + border-top: 3px solid #78dfb4; + border-radius: 999px; + background: transparent; + box-shadow: none; + transform: translateY(3px); +} + +.ops-state-success .ops-state-eye.left { + transform: translateY(3px) rotate(-8deg); +} + +.ops-state-success .ops-state-eye.right { + transform: translateY(3px) rotate(8deg); +} + +.ops-state-success .ops-state-eye::after { + display: none; +} + +.ops-state-error .ops-state-face { + box-shadow: + 0 8px 30px rgba(57, 19, 19, 0.32), + inset 0 0 0 1px rgba(255, 155, 155, 0.32); +} + +.ops-state-error .ops-state-eye { + width: 20px; + height: 20px; + border: 0; + background: transparent; + box-shadow: none; +} + +.ops-state-error .ops-state-eye::after, +.ops-state-error .ops-state-eye::before { + content: ""; + display: block; + position: absolute; + left: 50%; + top: 50%; + width: 18px; + height: 3px; + border-radius: 999px; + background: #ff9b9b; +} + +.ops-state-error .ops-state-eye::after { + transform: translate(-50%, -50%) rotate(45deg); +} + +.ops-state-error .ops-state-eye::before { + transform: translate(-50%, -50%) rotate(-45deg); +} + +.ops-state-error .ops-state-face, +.ops-state-stopped .ops-state-face, +.ops-state-exited .ops-state-face { + filter: saturate(0.75); +} + +.ops-state-stopped .ops-state-eye, +.ops-state-exited .ops-state-eye { + width: 24px; + height: 3px; + border: 0; + background: color-mix(in oklab, var(--muted) 68%, #9fb4dc 32%); + box-shadow: none; +} + +.ops-state-stopped .ops-state-eye::after, +.ops-state-exited .ops-state-eye::after { + display: none; +} + +.ops-state-unknown .ops-state-eye, +.ops-state-info .ops-state-eye { + animation: none; +} + +@keyframes ops-state-breathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +@keyframes ops-eye-scan { + 0%, 100% { transform: translate(-50%, -50%) translateX(0); } + 30% { transform: translate(-50%, -50%) translateX(-2px); } + 65% { transform: translate(-50%, -50%) translateX(2px); } +} + +@keyframes ops-eye-scan-fast { + 0%, 100% { transform: translate(-50%, -50%) translateX(0); } + 50% { transform: translate(-50%, -50%) translateX(2px); } +} + +@keyframes ops-state-orbit { + 0% { transform: rotate(0deg) translateX(20px) rotate(0deg); } + 100% { transform: rotate(360deg) translateX(20px) rotate(-360deg); } +} + +@keyframes ops-tool-bob { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(-15deg) translateY(-1px); } +} + +.ops-cron-list { + display: grid; + gap: 8px; +} + +.ops-cron-item { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; + border: 1px solid var(--line); + border-radius: 10px; + background: color-mix(in oklab, var(--panel-soft) 82%, transparent); + padding: 8px; +} + +.ops-cron-main { + min-width: 0; + display: grid; + gap: 4px; +} + +.ops-cron-name { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 700; + color: var(--text); +} + +.ops-cron-meta { + font-size: 12px; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ops-cron-actions { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.ops-runtime-row { + display: grid; + grid-template-columns: 90px 1fr; + gap: 8px; + align-items: start; + color: var(--text); + font-size: 13px; +} + +.ops-runtime-row span { + color: var(--subtitle); + font-weight: 700; +} + +.ops-preview { + border: 1px solid var(--line); + border-radius: 10px; + padding: 8px 10px; + background: var(--panel); + white-space: pre-wrap; + line-height: 1.5; + max-height: 140px; + overflow: auto; +} + +.ops-event-list { + display: grid; + gap: 8px; + max-height: 240px; + overflow: auto; +} + +.ops-event-item { + border: 1px solid var(--line); + border-radius: 10px; + padding: 8px; + display: grid; + grid-template-columns: 86px 1fr auto; + gap: 8px; + align-items: center; + background: var(--panel); +} + +.ops-event-state { + font-size: 11px; + font-weight: 800; + border-radius: 999px; + border: 1px solid; + padding: 2px 8px; + text-align: center; +} + +.ops-event-state.thinking { + color: #61a0ff; + border-color: #5c88d8; + background: rgba(95, 146, 229, 0.15); +} + +.ops-event-state.tool_call { + color: #4fd1b0; + border-color: #3ca98f; + background: rgba(60, 169, 143, 0.16); +} + +.ops-event-state.success { + color: #4fc790; + border-color: #2ca476; + background: rgba(44, 164, 118, 0.16); +} + +.ops-event-state.error { + color: #e56a6a; + border-color: #cf5555; + background: rgba(209, 75, 75, 0.2); +} + +.ops-event-state.info { + color: #7ea3e6; + border-color: #5e7fb8; + background: rgba(94, 127, 184, 0.16); +} + +.ops-event-text { + font-size: 12px; + color: var(--text); + line-height: 1.4; +} + +.ops-event-time { + font-size: 11px; + color: var(--subtitle); +} + +.ops-empty-inline { + color: var(--muted); + font-size: 13px; + padding: 4px 2px; +} + +.ops-action-grid { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.workspace-panel { + border: 1px solid var(--line); + border-radius: 10px; + overflow: hidden; + min-height: 340px; + display: grid; + grid-template-rows: 1fr auto; +} + +.workspace-toolbar { + display: flex; + gap: 8px; + align-items: center; +} + +.workspace-refresh-icon-btn { + width: 30px; + height: 30px; + border-radius: 8px; + border: 1px solid var(--line); + background: var(--panel-soft); + color: var(--icon); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease; +} + +.workspace-refresh-icon-btn:hover { + border-color: color-mix(in oklab, var(--brand) 60%, var(--line) 40%); + background: color-mix(in oklab, var(--brand-soft) 42%, var(--panel-soft) 58%); +} + +.workspace-refresh-icon-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.workspace-auto-switch { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--subtitle); + font-size: 11px; + font-weight: 700; + user-select: none; +} + +.workspace-auto-switch input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.workspace-auto-switch-track { + position: relative; + width: 34px; + height: 20px; + border-radius: 999px; + border: 1px solid var(--line); + background: color-mix(in oklab, var(--panel-soft) 85%, var(--panel) 15%); + transition: background 0.2s ease, border-color 0.2s ease; +} + +.workspace-auto-switch-track::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 14px; + height: 14px; + border-radius: 999px; + background: color-mix(in oklab, var(--text) 75%, #fff 25%); + transition: transform 0.2s ease, background 0.2s ease; +} + +.workspace-auto-switch input:checked + .workspace-auto-switch-track { + border-color: color-mix(in oklab, var(--brand) 70%, var(--line) 30%); + background: color-mix(in oklab, var(--brand) 42%, var(--panel-soft) 58%); +} + +.workspace-auto-switch input:checked + .workspace-auto-switch-track::after { + transform: translateX(14px); + background: #fff; +} + +.workspace-path { + border: 1px solid var(--line); + border-radius: 8px; + padding: 6px 8px; + font-size: 11px; + color: var(--muted); + background: var(--panel-soft); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.workspace-list { + background: var(--panel); + overflow: auto; + padding: 8px; +} + +.workspace-entry { + width: 100%; + border: 1px solid transparent; + border-radius: 8px; + background: color-mix(in oklab, var(--panel-soft) 70%, transparent); + color: var(--text); + text-align: left; + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 600; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 8px; + padding-right: 8px; + margin-bottom: 6px; +} + +.workspace-entry .workspace-entry-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workspace-entry .workspace-entry-meta { + color: var(--muted); + font-size: 11px; +} + +.workspace-entry.dir { + border-color: color-mix(in oklab, var(--line) 80%, transparent); +} + +.workspace-entry.nav-up { + border-style: dashed; +} + +.workspace-entry.file { + cursor: pointer; + border-color: color-mix(in oklab, var(--line) 80%, transparent); +} + +.workspace-entry:hover { + border-color: color-mix(in oklab, var(--brand) 65%, var(--line) 35%); + background: color-mix(in oklab, var(--brand-soft) 45%, transparent); +} + +.workspace-entry.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.workspace-hint { + background: var(--panel-soft); + color: var(--muted); + font-size: 12px; + font-weight: 600; + border-bottom: 1px solid var(--line); + padding: 7px 9px; +} + +.modal-preview { + width: min(1080px, 95vw); +} + +.workspace-preview-body { + border: 1px solid var(--line); + border-radius: 10px; + background: color-mix(in oklab, var(--panel-soft) 76%, var(--panel) 24%); + min-height: 52vh; + max-height: 72vh; + overflow: auto; +} + +.workspace-preview-embed { + width: 100%; + min-height: 68vh; + border: 0; + background: #fff; +} + +.workspace-preview-body pre { + margin: 0; + padding: 12px; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.56; + color: var(--text); + font-size: 13px; + font-family: 'SF Mono', Menlo, Consolas, monospace; +} + +.workspace-preview-body.markdown pre { + margin: 0; +} + +.workspace-markdown { + padding: 12px 14px; + color: var(--text); + line-height: 1.72; + font-size: 14px; + font-family: 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} + +.workspace-markdown > *:first-child { + margin-top: 0; +} + +.workspace-markdown > *:last-child { + margin-bottom: 0; +} + +.workspace-markdown h1, +.workspace-markdown h2, +.workspace-markdown h3, +.workspace-markdown h4 { + margin: 16px 0 8px; + color: var(--text); + font-weight: 800; + line-height: 1.3; +} + +.workspace-markdown h1 { + font-size: 24px; +} + +.workspace-markdown h2 { + font-size: 20px; +} + +.workspace-markdown h3 { + font-size: 17px; +} + +.workspace-markdown p { + margin: 8px 0; +} + +.workspace-markdown ul, +.workspace-markdown ol { + margin: 8px 0 8px 20px; + padding: 0; +} + +.workspace-markdown li { + margin: 4px 0; +} + +.workspace-markdown blockquote { + margin: 10px 0; + padding: 8px 12px; + border-left: 3px solid color-mix(in oklab, var(--brand) 50%, var(--line) 50%); + background: color-mix(in oklab, var(--panel-soft) 76%, var(--panel) 24%); + border-radius: 8px; +} + +.workspace-markdown hr { + border: 0; + border-top: 1px solid var(--line); + margin: 14px 0; +} + +.workspace-markdown code { + font-family: 'SF Mono', Menlo, Consolas, monospace; + background: color-mix(in oklab, var(--panel-soft) 76%, var(--panel) 24%); + border: 1px solid var(--line); + border-radius: 6px; + padding: 1px 6px; + font-size: 12px; +} + +.workspace-markdown pre { + margin: 10px 0; + padding: 10px 12px; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.55; + background: color-mix(in oklab, var(--panel-soft) 70%, var(--panel) 30%); + border: 1px solid var(--line); + border-radius: 8px; +} + +.workspace-markdown pre code { + border: none; + background: transparent; + padding: 0; + font-size: 12px; +} + +.workspace-markdown table { + width: 100%; + border-collapse: collapse; + margin: 10px 0; + font-size: 13px; +} + +.workspace-markdown th, +.workspace-markdown td { + border: 1px solid var(--line); + padding: 6px 8px; + text-align: left; +} + +.workspace-markdown th { + background: color-mix(in oklab, var(--panel-soft) 74%, var(--panel) 26%); +} + +.workspace-markdown a { + color: color-mix(in oklab, var(--brand) 80%, #5fa6ff 20%); + text-decoration: underline; +} + +.workspace-preview-meta { + color: var(--muted); + font-size: 11px; +} + +.app-shell[data-theme='dark'] .ops-chat-bubble.assistant { + background: #112850; +} + +.app-shell[data-theme='dark'] .ops-chat-bubble.user { + background: #0f2f35; +} + +.app-shell[data-theme='light'] .ops-chat-bubble.assistant { + background: #eaf1ff; + border-color: #a9c1ee; +} + +.app-shell[data-theme='light'] .ops-chat-bubble.user { + background: #e8f6f2; + border-color: #9ccfc2; +} + +.app-shell[data-theme='light'] .ops-avatar.bot { + background: #e6efff; + border-color: #a5bfe8; +} + +.app-shell[data-theme='light'] .ops-avatar.user { + background: #edf4ff; + border-color: #b1c7ea; + color: #274a7d; +} + +.app-shell[data-theme='light'] .ops-chat-scroll, +.app-shell[data-theme='light'] .ops-chat-head, +.app-shell[data-theme='light'] .ops-composer, +.app-shell[data-theme='light'] .ops-bot-card, +.app-shell[data-theme='light'] .ops-runtime-card { + background: #f7fbff; +} + +.app-shell[data-theme='light'] .ops-preview, +.app-shell[data-theme='light'] .ops-status-pill { + background: #ffffff; +} + +.app-shell[data-theme='light'] .workspace-list { + background: #ffffff; +} + +.app-shell[data-theme='light'] .workspace-hint, +.app-shell[data-theme='light'] .workspace-path { + background: #f7fbff; +} + +@media (max-width: 1160px) { + .ops-chat-scroll { + min-height: 320px; + max-height: 55vh; + } + + .ops-chat-bubble { + max-width: 96%; + } + + .ops-composer { + grid-template-columns: 1fr; + } + + .workspace-panel { + min-height: 280px; + grid-template-rows: 1fr auto; + } + + .workspace-toolbar { + gap: 6px; + } + + .workspace-path { + flex: 1; + } + + .ops-runtime-state-card { + min-height: 0; + } + + .ops-runtime-state-card:not(.is-visual) { + min-height: 170px; + } + + .ops-runtime-state-card.is-visual { + min-height: 170px; + } + + .ops-runtime-state-card.is-visual .ops-state-stage { + min-height: 100%; + } + + .ops-runtime-state-card.is-visual .ops-state-face { + width: 172px; + height: 114px; + gap: 28px; + } + + .ops-runtime-state-card.is-visual .ops-state-eye { + width: 24px; + height: 24px; + } + + .ops-runtime-state-card.is-visual .ops-state-idle .ops-state-eye { + width: 30px; + height: 5px; + } +} diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx new file mode 100644 index 0000000..b17f266 --- /dev/null +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -0,0 +1,2281 @@ +import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react'; +import axios from 'axios'; +import { Activity, Boxes, Check, Clock3, EllipsisVertical, Eye, EyeOff, FileText, FolderOpen, Hammer, MessageSquareText, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { APP_ENDPOINTS } from '../../config/env'; +import { useAppStore } from '../../store/appStore'; +import type { ChatMessage } from '../../types/bot'; +import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgressText } from './messageParser'; +import nanobotLogo from '../../assets/nanobot-logo.png'; +import './BotDashboardModule.css'; +import { channelsZhCn } from '../../i18n/channels.zh-cn'; +import { channelsEn } from '../../i18n/channels.en'; +import { pickLocale } from '../../i18n'; +import { dashboardZhCn } from '../../i18n/dashboard.zh-cn'; +import { dashboardEn } from '../../i18n/dashboard.en'; +import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; + +interface BotDashboardModuleProps { + onOpenCreateWizard?: () => void; + onOpenImageFactory?: () => void; + forcedBotId?: string; + compactMode?: boolean; +} + +type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY'; +type WorkspaceNodeType = 'dir' | 'file'; +type ChannelType = 'dashboard' | 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack'; +type RuntimeViewMode = 'visual' | 'text'; +type CompactPanelTab = 'chat' | 'runtime'; + +interface WorkspaceNode { + name: string; + path: string; + type: WorkspaceNodeType; + size?: number; + ext?: string; + mtime?: string; + children?: WorkspaceNode[]; +} + +interface WorkspaceTreeResponse { + bot_id: string; + root: string; + cwd: string; + parent: string | null; + entries: WorkspaceNode[]; +} + +interface WorkspaceFileResponse { + bot_id: string; + path: string; + size: number; + is_markdown: boolean; + truncated: boolean; + content: string; +} + +interface WorkspacePreviewState { + path: string; + content: string; + truncated: boolean; + ext: string; + isMarkdown: boolean; +} + +interface WorkspaceUploadResponse { + bot_id: string; + files: Array<{ name: string; path: string; size: number }>; +} + +interface CronJob { + id: string; + name: string; + enabled?: boolean; + schedule?: { + kind?: 'at' | 'every' | 'cron' | string; + atMs?: number; + everyMs?: number; + expr?: string; + tz?: string; + }; + payload?: { + message?: string; + channel?: string; + to?: string; + }; + state?: { + nextRunAtMs?: number; + lastRunAtMs?: number; + lastStatus?: string; + lastError?: string; + }; +} + +interface CronJobsResponse { + bot_id: string; + version: number; + jobs: CronJob[]; +} + +interface BotChannel { + id: string | number; + bot_id: string; + channel_type: ChannelType | string; + external_app_id: string; + app_secret: string; + internal_port: number; + is_active: boolean; + extra_config: Record; + locked?: boolean; +} + +interface WorkspaceSkillOption { + id: string; + name: string; + type: 'dir' | 'file' | string; + path: string; + size?: number | null; + mtime?: string; + description?: string; +} + +interface SkillUploadResponse { + status: string; + bot_id: string; + installed: string[]; + skills: WorkspaceSkillOption[]; +} + +type BotEnvParams = Record; + +const providerPresets: Record = { + openrouter: { + model: 'openai/gpt-4o-mini', + apiBase: 'https://openrouter.ai/api/v1', + note: { + 'zh-cn': 'OpenRouter 网关,模型示例 openai/gpt-4o-mini', + en: 'OpenRouter gateway, model example: openai/gpt-4o-mini', + }, + }, + dashscope: { + model: 'qwen-plus', + apiBase: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + note: { + 'zh-cn': '阿里云 DashScope(千问),模型示例 qwen-plus', + en: 'Alibaba DashScope (Qwen), model example: qwen-plus', + }, + }, + openai: { + model: 'gpt-4o-mini', + note: { + 'zh-cn': 'OpenAI 原生接口', + en: 'OpenAI native endpoint', + }, + }, + deepseek: { + model: 'deepseek-chat', + note: { + 'zh-cn': 'DeepSeek 原生接口', + en: 'DeepSeek native endpoint', + }, + }, + kimi: { + model: 'moonshot-v1-8k', + apiBase: 'https://api.moonshot.cn/v1', + note: { + 'zh-cn': 'Kimi(Moonshot)接口,模型示例 moonshot-v1-8k', + en: 'Kimi (Moonshot) endpoint, model example: moonshot-v1-8k', + }, + }, + minimax: { + model: 'MiniMax-Text-01', + apiBase: 'https://api.minimax.chat/v1', + note: { + 'zh-cn': 'MiniMax 接口,模型示例 MiniMax-Text-01', + en: 'MiniMax endpoint, model example: MiniMax-Text-01', + }, + }, +}; +const optionalChannelTypes: ChannelType[] = ['feishu', 'qq', 'dingtalk', 'telegram', 'slack']; + +function formatClock(ts: number) { + const d = new Date(ts); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; +} + +function stateLabel(s?: string) { + return (s || 'IDLE').toUpperCase(); +} + +function normalizeRuntimeState(s?: string) { + const raw = stateLabel(s); + if (raw.includes('ERROR') || raw.includes('FAIL')) return 'ERROR'; + if (raw.includes('TOOL') || raw.includes('EXEC') || raw.includes('ACTION')) return 'TOOL_CALL'; + if (raw.includes('THINK') || raw.includes('PLAN') || raw.includes('REASON') || raw === 'RUNNING') return 'THINKING'; + if (raw.includes('SUCCESS') || raw.includes('DONE') || raw.includes('COMPLETE')) return 'SUCCESS'; + if (raw.includes('IDLE') || raw.includes('STOP')) return 'IDLE'; + return raw; +} + +function isPreviewableWorkspaceFile(node: WorkspaceNode) { + if (node.type !== 'file') return false; + const ext = (node.ext || '').toLowerCase(); + return ['.md', '.json', '.log', '.txt', '.csv', '.pdf'].includes(ext); +} + +function isPdfPath(path: string) { + return String(path || '').trim().toLowerCase().endsWith('.pdf'); +} + +function normalizeAttachmentPaths(raw: unknown): string[] { + if (!Array.isArray(raw)) return []; + return raw + .map((v) => String(v || '').trim().replace(/\\/g, '/')) + .filter((v) => v.length > 0); +} + +function normalizeDashboardAttachmentPath(path: string): string { + const v = String(path || '').trim().replace(/\\/g, '/'); + if (!v) return ''; + const prefix = '/root/.nanobot/workspace/'; + if (v.startsWith(prefix)) return v.slice(prefix.length); + return v.replace(/^\/+/, ''); +} + +function isExternalHttpLink(href: string): boolean { + return /^https?:\/\//i.test(String(href || '').trim()); +} + +function mergeConversation(messages: ChatMessage[]) { + const merged: ChatMessage[] = []; + messages + .filter((msg) => msg.role !== 'system' && (msg.text.trim().length > 0 || (msg.attachments || []).length > 0)) + .forEach((msg) => { + const cleanText = msg.role === 'user' ? normalizeUserMessageText(msg.text) : normalizeAssistantMessageText(msg.text); + const attachments = normalizeAttachmentPaths(msg.attachments).map(normalizeDashboardAttachmentPath).filter(Boolean); + if (!cleanText && attachments.length === 0) return; + const last = merged[merged.length - 1]; + if (last && last.role === msg.role) { + const normalizedLast = last.role === 'user' ? normalizeUserMessageText(last.text) : normalizeAssistantMessageText(last.text); + const normalizedCurrent = msg.role === 'user' ? normalizeUserMessageText(cleanText) : normalizeAssistantMessageText(cleanText); + const lastKind = last.kind || 'final'; + const currentKind = msg.kind || 'final'; + const sameAttachmentSet = + JSON.stringify(normalizeAttachmentPaths(last.attachments)) === JSON.stringify(attachments); + if (lastKind === currentKind && normalizedLast === normalizedCurrent && sameAttachmentSet && Math.abs(msg.ts - last.ts) < 15000) { + last.ts = msg.ts; + return; + } + } + merged.push({ ...msg, text: cleanText, attachments }); + }); + return merged.slice(-120); +} + +function clampTemperature(value: number) { + if (Number.isNaN(value)) return 0.2; + return Math.min(1, Math.max(0, value)); +} + +function formatCronSchedule(job: CronJob, isZh: boolean) { + const s = job.schedule || {}; + if (s.kind === 'every' && Number(s.everyMs) > 0) { + const sec = Math.round(Number(s.everyMs) / 1000); + return isZh ? `每 ${sec}s` : `every ${sec}s`; + } + if (s.kind === 'cron') { + if (s.tz) return `${s.expr || '-'} (${s.tz})`; + return s.expr || '-'; + } + if (s.kind === 'at' && Number(s.atMs) > 0) { + return new Date(Number(s.atMs)).toLocaleString(); + } + return '-'; +} + +export function BotDashboardModule({ + onOpenCreateWizard, + onOpenImageFactory, + forcedBotId, + compactMode = false, +}: BotDashboardModuleProps) { + const { + activeBots, + setBots, + updateBotStatus, + locale, + addBotMessage, + setBotMessages, + } = useAppStore(); + const { notify, confirm } = useLucentPrompt(); + const [selectedBotId, setSelectedBotId] = useState(''); + const [command, setCommand] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [showBaseModal, setShowBaseModal] = useState(false); + const [showParamModal, setShowParamModal] = useState(false); + const [showChannelModal, setShowChannelModal] = useState(false); + const [showSkillsModal, setShowSkillsModal] = useState(false); + const [showEnvParamsModal, setShowEnvParamsModal] = useState(false); + const [showCronModal, setShowCronModal] = useState(false); + const [showAgentModal, setShowAgentModal] = useState(false); + const [agentTab, setAgentTab] = useState('AGENTS'); + const [isTestingProvider, setIsTestingProvider] = useState(false); + const [providerTestResult, setProviderTestResult] = useState(''); + const [operatingBotId, setOperatingBotId] = useState(null); + const [sendingByBot, setSendingByBot] = useState>({}); + const [controlStateByBot, setControlStateByBot] = useState>({}); + const chatBottomRef = useRef(null); + const [workspaceEntries, setWorkspaceEntries] = useState([]); + const [workspaceLoading, setWorkspaceLoading] = useState(false); + const [workspaceError, setWorkspaceError] = useState(''); + const [workspaceCurrentPath, setWorkspaceCurrentPath] = useState(''); + const [workspaceParentPath, setWorkspaceParentPath] = useState(null); + const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false); + const [workspacePreview, setWorkspacePreview] = useState(null); + const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true); + const [pendingAttachments, setPendingAttachments] = useState([]); + const [isUploadingAttachments, setIsUploadingAttachments] = useState(false); + const filePickerRef = useRef(null); + const [cronJobs, setCronJobs] = useState([]); + const [cronLoading, setCronLoading] = useState(false); + const [cronActionJobId, setCronActionJobId] = useState(''); + const [channels, setChannels] = useState([]); + const [botSkills, setBotSkills] = useState([]); + const [isSkillUploading, setIsSkillUploading] = useState(false); + const skillZipPickerRef = useRef(null); + const [envParams, setEnvParams] = useState({}); + const [envDraftKey, setEnvDraftKey] = useState(''); + const [envDraftValue, setEnvDraftValue] = useState(''); + const [envDraftVisible, setEnvDraftVisible] = useState(false); + const [envVisibleByKey, setEnvVisibleByKey] = useState>({}); + const [isSavingChannel, setIsSavingChannel] = useState(false); + const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false); + const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({ + sendProgress: false, + sendToolHints: false, + }); + const [newChannelType, setNewChannelType] = useState('feishu'); + const [runtimeViewMode, setRuntimeViewMode] = useState('visual'); + const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false); + const [compactPanelTab, setCompactPanelTab] = useState('chat'); + const [isCompactMobile, setIsCompactMobile] = useState(false); + const runtimeMenuRef = useRef(null); + const markdownComponents = useMemo( + () => ({ + a: ({ href, children, ...props }: AnchorHTMLAttributes) => { + const link = String(href || '').trim(); + if (isExternalHttpLink(link)) { + return ( +
+ {children} + + ); + } + return ( + + {children} + + ); + }, + }), + [], + ); + + const [editForm, setEditForm] = useState({ + name: '', + llm_provider: '', + llm_model: '', + image_tag: '', + api_key: '', + api_base: '', + temperature: 0.2, + top_p: 1, + max_tokens: 8192, + agents_md: '', + soul_md: '', + user_md: '', + tools_md: '', + identity_md: '', + }); + + const bots = useMemo(() => Object.values(activeBots), [activeBots]); + const selectedBot = selectedBotId ? activeBots[selectedBotId] : undefined; + const forcedBotMissing = Boolean(forcedBotId && bots.length > 0 && !activeBots[String(forcedBotId).trim()]); + const messages = selectedBot?.messages || []; + const events = selectedBot?.events || []; + const isZh = locale === 'zh'; + const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const }); + const t = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn }); + const lc = isZh ? channelsZhCn : channelsEn; + const runtimeMoreLabel = isZh ? '更多' : 'More'; + const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined; + const isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false; + const canChat = Boolean(selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState); + const isChatEnabled = Boolean(canChat && !isSending); + + const conversation = useMemo(() => mergeConversation(messages), [messages]); + const latestEvent = useMemo(() => [...events].reverse()[0], [events]); + const workspaceFiles = useMemo( + () => workspaceEntries.filter((v) => v.type === 'file' && isPreviewableWorkspaceFile(v)), + [workspaceEntries], + ); + const addableChannelTypes = useMemo(() => { + const exists = new Set(channels.map((c) => String(c.channel_type).toLowerCase())); + return optionalChannelTypes.filter((t) => !exists.has(t)); + }, [channels]); + const envEntries = useMemo( + () => + Object.entries(envParams || {}) + .filter(([k]) => String(k || '').trim().length > 0) + .sort(([a], [b]) => a.localeCompare(b)), + [envParams], + ); + const lastUserTs = useMemo(() => [...conversation].reverse().find((m) => m.role === 'user')?.ts || 0, [conversation]); + const lastAssistantFinalTs = useMemo( + () => + [...conversation].reverse().find((m) => m.role === 'assistant' && (m.kind || 'final') !== 'progress')?.ts || 0, + [conversation], + ); + const isThinking = useMemo(() => { + if (!selectedBot || selectedBot.docker_status !== 'RUNNING') return false; + if (lastUserTs <= 0) return false; + if (lastAssistantFinalTs >= lastUserTs) return false; + // Keep showing running/thinking state until a final assistant reply arrives. + return true; + }, [selectedBot, lastUserTs, lastAssistantFinalTs]); + const displayState = useMemo(() => { + if (!selectedBot) return 'IDLE'; + const backendState = normalizeRuntimeState(selectedBot.current_state); + if (selectedBot.docker_status !== 'RUNNING') return backendState; + + if (backendState === 'TOOL_CALL' || backendState === 'THINKING' || backendState === 'ERROR') { + return backendState; + } + + if (isThinking) { + if (latestEvent?.state === 'TOOL_CALL') return 'TOOL_CALL'; + return 'THINKING'; + } + + if ( + latestEvent && + ['THINKING', 'TOOL_CALL', 'ERROR'].includes(latestEvent.state) && + Date.now() - latestEvent.ts < 15000 + ) { + return latestEvent.state; + } + + if (latestEvent?.state === 'ERROR') return 'ERROR'; + return 'IDLE'; + }, [selectedBot, isThinking, latestEvent]); + const runtimeAction = useMemo(() => { + const action = summarizeProgressText(selectedBot?.last_action || '', isZh); + if (action && action !== t.processing) return action; + const eventText = summarizeProgressText(latestEvent?.text || '', isZh); + if (eventText && eventText !== t.processing) return eventText; + return '-'; + }, [selectedBot, latestEvent, isZh, t.processing]); + const conversationNodes = useMemo( + () => + conversation.map((item, idx) => ( +
+
+ {item.role !== 'user' && ( +
+ Nanobot +
+ )} + +
+
+ {item.role === 'user' ? t.you : 'Nanobot'} + {formatClock(item.ts)} +
+
+ {item.text ? ( + item.role === 'user' ? ( +
{normalizeUserMessageText(item.text)}
+ ) : ( + + {item.text} + + ) + ) : null} + {(item.attachments || []).length > 0 ? ( +
+ {(item.attachments || []).map((rawPath) => { + const filePath = normalizeDashboardAttachmentPath(rawPath); + const isPdf = isPdfPath(filePath); + const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${isPdf ? '&download=1' : ''}`; + const filename = filePath.split('/').pop() || filePath; + return ( + + {filename} + + ); + })} +
+ ) : null} +
+
+ + {item.role === 'user' && ( +
+ +
+ )} +
+
+ )), + [conversation, selectedBotId, t.user, t.you], + ); + + useEffect(() => { + const forced = String(forcedBotId || '').trim(); + if (forced) { + if (activeBots[forced]) { + if (selectedBotId !== forced) setSelectedBotId(forced); + } else if (selectedBotId) { + setSelectedBotId(''); + } + return; + } + if (!selectedBotId && bots.length > 0) setSelectedBotId(bots[0].id); + if (selectedBotId && !activeBots[selectedBotId] && bots.length > 0) setSelectedBotId(bots[0].id); + }, [bots, selectedBotId, activeBots, forcedBotId]); + + useEffect(() => { + chatBottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + }, [selectedBotId, conversation.length]); + + useEffect(() => { + const onPointerDown = (event: MouseEvent) => { + if (!runtimeMenuRef.current) return; + if (!runtimeMenuRef.current.contains(event.target as Node)) { + setRuntimeMenuOpen(false); + } + }; + document.addEventListener('mousedown', onPointerDown); + return () => document.removeEventListener('mousedown', onPointerDown); + }, []); + + useEffect(() => { + setRuntimeMenuOpen(false); + }, [selectedBotId]); + + useEffect(() => { + if (!compactMode) { + setIsCompactMobile(false); + setCompactPanelTab('chat'); + return; + } + const media = window.matchMedia('(max-width: 980px)'); + const apply = () => setIsCompactMobile(media.matches); + apply(); + media.addEventListener('change', apply); + return () => media.removeEventListener('change', apply); + }, [compactMode]); + + useEffect(() => { + if (!selectedBotId) return; + const bot = selectedBot; + if (!bot) return; + setProviderTestResult(''); + setEditForm({ + name: bot.name || '', + 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: bot.max_tokens ?? 8192, + 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 || '', + }); + setPendingAttachments([]); + }, [selectedBotId, selectedBot?.id]); + + useEffect(() => { + if (!selectedBotId || !selectedBot) { + setGlobalDelivery({ sendProgress: false, sendToolHints: false }); + return; + } + setGlobalDelivery({ + sendProgress: Boolean(selectedBot.send_progress), + sendToolHints: Boolean(selectedBot.send_tool_hints), + }); + }, [selectedBotId, selectedBot?.send_progress, selectedBot?.send_tool_hints]); + + const refresh = async () => { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`); + setBots(res.data); + }; + + const openWorkspaceFilePreview = async (path: string) => { + if (!selectedBotId || !path) return; + const normalizedPath = String(path || '').trim(); + if (isPdfPath(normalizedPath)) { + const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(normalizedPath)}&download=1`; + window.open(href, '_blank', 'noopener,noreferrer'); + return; + } + setWorkspaceFileLoading(true); + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/file`, { + params: { path, max_bytes: 400000 }, + }); + const filePath = res.data.path || path; + const textExt = (filePath.split('.').pop() || '').toLowerCase(); + let content = res.data.content || ''; + if (textExt === 'json') { + try { + content = JSON.stringify(JSON.parse(content), null, 2); + } catch { + // Keep original content when JSON is not strictly parseable. + } + } + setWorkspacePreview({ + path: filePath, + content, + truncated: Boolean(res.data.truncated), + ext: textExt ? `.${textExt}` : '', + isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown), + }); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.fileReadFail; + notify(msg, { tone: 'error' }); + } finally { + setWorkspaceFileLoading(false); + } + }; + + const loadWorkspaceTree = async (botId: string, path: string = '') => { + if (!botId) return; + setWorkspaceLoading(true); + setWorkspaceError(''); + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/workspace/tree`, { + params: { path }, + }); + const entries = Array.isArray(res.data?.entries) ? res.data.entries : []; + setWorkspaceEntries(entries); + setWorkspaceCurrentPath(res.data?.cwd || ''); + setWorkspaceParentPath(res.data?.parent ?? null); + } catch (error: any) { + setWorkspaceEntries([]); + setWorkspaceCurrentPath(''); + setWorkspaceParentPath(null); + setWorkspaceError(error?.response?.data?.detail || t.workspaceLoadFail); + } finally { + setWorkspaceLoading(false); + } + }; + + const loadChannels = async (botId: string) => { + if (!botId) return; + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`); + setChannels(Array.isArray(res.data) ? res.data : []); + }; + + const loadBotSkills = async (botId: string) => { + if (!botId) return; + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/skills`); + setBotSkills(Array.isArray(res.data) ? res.data : []); + }; + + const loadBotEnvParams = async (botId: string) => { + if (!botId) return; + try { + const res = await axios.get<{ env_params?: Record }>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/env-params`); + const rows = res.data?.env_params && typeof res.data.env_params === 'object' ? res.data.env_params : {}; + const next: BotEnvParams = {}; + Object.entries(rows).forEach(([k, v]) => { + const key = String(k || '').trim().toUpperCase(); + if (!key) return; + next[key] = String(v ?? ''); + }); + setEnvParams(next); + } catch { + setEnvParams({}); + } + }; + + const saveBotEnvParams = async () => { + if (!selectedBot) return; + try { + await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/env-params`, { env_params: envParams }); + setShowEnvParamsModal(false); + notify(t.envParamsSaved, { tone: 'success' }); + } catch (error: any) { + notify(error?.response?.data?.detail || t.envParamsSaveFail, { tone: 'error' }); + } + }; + + const upsertEnvParam = (key: string, value: string) => { + const normalized = String(key || '').trim().toUpperCase(); + if (!normalized) return; + setEnvParams((prev) => ({ ...(prev || {}), [normalized]: String(value ?? '') })); + }; + + const removeEnvParam = (key: string) => { + const normalized = String(key || '').trim().toUpperCase(); + if (!normalized) return; + setEnvParams((prev) => { + const next = { ...(prev || {}) }; + delete next[normalized]; + return next; + }); + }; + + const removeBotSkill = async (skill: WorkspaceSkillOption) => { + if (!selectedBot) return; + const ok = await confirm({ + title: t.removeSkill, + message: t.toolsRemoveConfirm(skill.name || skill.id), + tone: 'warning', + }); + if (!ok) return; + try { + await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/skills/${encodeURIComponent(skill.id)}`); + await loadBotSkills(selectedBot.id); + } catch (error: any) { + notify(error?.response?.data?.detail || t.toolsRemoveFail, { tone: 'error' }); + } + }; + + const triggerSkillZipUpload = () => { + if (!selectedBot || isSkillUploading) return; + skillZipPickerRef.current?.click(); + }; + + const onPickSkillZip = async (event: ChangeEvent) => { + if (!selectedBot || !event.target.files || event.target.files.length === 0) return; + const file = event.target.files[0]; + const filename = String(file?.name || '').toLowerCase(); + if (!filename.endsWith('.zip')) { + notify(t.invalidZipFile, { tone: 'warning' }); + event.target.value = ''; + return; + } + const formData = new FormData(); + formData.append('file', file); + setIsSkillUploading(true); + try { + const res = await axios.post( + `${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/skills/upload`, + formData, + ); + const nextSkills = Array.isArray(res.data?.skills) ? res.data.skills : []; + setBotSkills(nextSkills); + } catch (error: any) { + notify(error?.response?.data?.detail || t.toolsAddFail, { tone: 'error' }); + } finally { + setIsSkillUploading(false); + event.target.value = ''; + } + }; + + const loadCronJobs = async (botId: string) => { + if (!botId) return; + setCronLoading(true); + try { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/cron/jobs`, { + params: { include_disabled: true }, + }); + setCronJobs(Array.isArray(res.data?.jobs) ? res.data.jobs : []); + } catch { + setCronJobs([]); + } finally { + setCronLoading(false); + } + }; + + const stopCronJob = async (jobId: string) => { + if (!selectedBot || !jobId) return; + setCronActionJobId(jobId); + try { + await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/cron/jobs/${jobId}/stop`); + await loadCronJobs(selectedBot.id); + } catch (error: any) { + notify(error?.response?.data?.detail || t.cronStopFail, { tone: 'error' }); + } finally { + setCronActionJobId(''); + } + }; + + const deleteCronJob = async (jobId: string) => { + if (!selectedBot || !jobId) return; + const ok = await confirm({ + title: t.cronDelete, + message: t.cronDeleteConfirm(jobId), + tone: 'warning', + }); + if (!ok) return; + setCronActionJobId(jobId); + try { + await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/cron/jobs/${jobId}`); + await loadCronJobs(selectedBot.id); + } catch (error: any) { + notify(error?.response?.data?.detail || t.cronDeleteFail, { tone: 'error' }); + } finally { + setCronActionJobId(''); + } + }; + + const updateChannelLocal = (index: number, patch: Partial) => { + setChannels((prev) => prev.map((c, i) => (i === index ? { ...c, ...patch } : c))); + }; + + const saveChannel = async (channel: BotChannel) => { + if (!selectedBot) return; + setIsSavingChannel(true); + try { + await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/channels/${channel.id}`, { + channel_type: channel.channel_type, + external_app_id: channel.external_app_id, + app_secret: channel.app_secret, + internal_port: Number(channel.internal_port), + is_active: channel.is_active, + extra_config: sanitizeChannelExtra(String(channel.channel_type), channel.extra_config || {}), + }); + await loadChannels(selectedBot.id); + notify(t.channelSaved, { tone: 'success' }); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.channelSaveFail; + notify(msg, { tone: 'error' }); + } finally { + setIsSavingChannel(false); + } + }; + + const addChannel = async () => { + if (!selectedBot || !addableChannelTypes.includes(newChannelType)) return; + setIsSavingChannel(true); + try { + await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/channels`, { + channel_type: newChannelType, + is_active: true, + external_app_id: '', + app_secret: '', + internal_port: 8080, + extra_config: {}, + }); + await loadChannels(selectedBot.id); + const rest = addableChannelTypes.filter((t) => t !== newChannelType); + if (rest.length > 0) setNewChannelType(rest[0]); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.channelAddFail; + notify(msg, { tone: 'error' }); + } finally { + setIsSavingChannel(false); + } + }; + + const removeChannel = async (channel: BotChannel) => { + if (!selectedBot || channel.channel_type === 'dashboard') return; + const ok = await confirm({ + title: t.channels, + message: t.channelDeleteConfirm(channel.channel_type), + tone: 'warning', + }); + if (!ok) return; + setIsSavingChannel(true); + try { + await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/channels/${channel.id}`); + await loadChannels(selectedBot.id); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.channelDeleteFail; + notify(msg, { tone: 'error' }); + } finally { + setIsSavingChannel(false); + } + }; + + const isDashboardChannel = (channel: BotChannel) => String(channel.channel_type).toLowerCase() === 'dashboard'; + const sanitizeChannelExtra = (channelType: string, extra: Record) => { + const type = String(channelType || '').toLowerCase(); + if (type === 'dashboard') return extra || {}; + const next = { ...(extra || {}) }; + delete next.sendProgress; + delete next.sendToolHints; + return next; + }; + const updateGlobalDeliveryFlag = (key: 'sendProgress' | 'sendToolHints', value: boolean) => { + setGlobalDelivery((prev) => ({ ...prev, [key]: value })); + }; + const saveGlobalDelivery = async () => { + if (!selectedBot) return; + setIsSavingGlobalDelivery(true); + try { + await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}`, { + send_progress: Boolean(globalDelivery.sendProgress), + send_tool_hints: Boolean(globalDelivery.sendToolHints), + }); + await refresh(); + notify(t.channelSaved, { tone: 'success' }); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.channelSaveFail; + notify(msg, { tone: 'error' }); + } finally { + setIsSavingGlobalDelivery(false); + } + }; + + const renderChannelFields = (channel: BotChannel, idx: number) => { + const ctype = String(channel.channel_type).toLowerCase(); + if (ctype === 'telegram') { + return ( + <> + updateChannelLocal(idx, { app_secret: e.target.value })} /> + updateChannelLocal(idx, { extra_config: { ...(channel.extra_config || {}), proxy: e.target.value } })} + /> + + + ); + } + + if (ctype === 'feishu') { + return ( + <> + updateChannelLocal(idx, { external_app_id: e.target.value })} /> + updateChannelLocal(idx, { app_secret: e.target.value })} /> + updateChannelLocal(idx, { extra_config: { ...(channel.extra_config || {}), encryptKey: e.target.value } })} /> + updateChannelLocal(idx, { extra_config: { ...(channel.extra_config || {}), verificationToken: e.target.value } })} /> + + ); + } + + if (ctype === 'dingtalk') { + return ( + <> + updateChannelLocal(idx, { external_app_id: e.target.value })} /> + updateChannelLocal(idx, { app_secret: e.target.value })} /> + + ); + } + + if (ctype === 'slack') { + return ( + <> + updateChannelLocal(idx, { external_app_id: e.target.value })} /> + updateChannelLocal(idx, { app_secret: e.target.value })} /> + + ); + } + + if (ctype === 'qq') { + return ( + <> + updateChannelLocal(idx, { external_app_id: e.target.value })} /> + updateChannelLocal(idx, { app_secret: e.target.value })} /> + + ); + } + + return null; + }; + + const stopBot = async (id: string, status: string) => { + if (status !== 'RUNNING') return; + setOperatingBotId(id); + setControlStateByBot((prev) => ({ ...prev, [id]: 'stopping' })); + try { + await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/stop`); + updateBotStatus(id, 'STOPPED'); + await refresh(); + } catch { + notify(t.stopFail, { tone: 'error' }); + } finally { + setOperatingBotId(null); + setControlStateByBot((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + } + }; + + const startBot = async (id: string, status: string) => { + if (status === 'RUNNING') return; + setOperatingBotId(id); + setControlStateByBot((prev) => ({ ...prev, [id]: 'starting' })); + try { + await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/start`); + updateBotStatus(id, 'RUNNING'); + await refresh(); + } catch { + notify(t.startFail, { tone: 'error' }); + } finally { + setOperatingBotId(null); + setControlStateByBot((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + } + }; + + const send = async () => { + if (!selectedBot || !canChat || isSending) return; + if (!command.trim() && pendingAttachments.length === 0) return; + const text = normalizeUserMessageText(command); + const payloadText = text || (pendingAttachments.length > 0 ? t.attachmentMessage : ''); + if (!payloadText && pendingAttachments.length === 0) return; + + try { + setSendingByBot((prev) => ({ ...prev, [selectedBot.id]: true })); + const res = await axios.post( + `${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`, + { command: payloadText, attachments: pendingAttachments }, + { timeout: 12000 }, + ); + if (!res.data?.success) { + throw new Error(t.backendDeliverFail); + } + setCommand(''); + setPendingAttachments([]); + } catch (error: any) { + const msg = error?.response?.data?.detail || error?.message || t.sendFail; + addBotMessage(selectedBot.id, { + role: 'assistant', + text: t.sendFailMsg(msg), + ts: Date.now(), + }); + notify(msg, { tone: 'error' }); + } finally { + setSendingByBot((prev) => { + const next = { ...prev }; + delete next[selectedBot.id]; + return next; + }); + } + }; + + const onComposerKeyDown = (e: KeyboardEvent) => { + const native = e.nativeEvent as unknown as { isComposing?: boolean; keyCode?: number }; + if (native.isComposing || native.keyCode === 229) return; + const isEnter = e.key === 'Enter' || e.key === 'NumpadEnter'; + if (!isEnter || e.shiftKey) return; + e.preventDefault(); + void send(); + }; + + const triggerPickAttachments = () => { + if (!selectedBot || !canChat || isUploadingAttachments) return; + filePickerRef.current?.click(); + }; + + const onPickAttachments = async (event: ChangeEvent) => { + if (!selectedBot || !event.target.files || event.target.files.length === 0) return; + const files = Array.from(event.target.files); + const formData = new FormData(); + files.forEach((file) => formData.append('files', file)); + formData.append('path', 'uploads'); + + setIsUploadingAttachments(true); + try { + const res = await axios.post( + `${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/workspace/upload`, + formData, + ); + const uploaded = normalizeAttachmentPaths((res.data?.files || []).map((v) => v.path)); + if (uploaded.length > 0) { + setPendingAttachments((prev) => Array.from(new Set([...prev, ...uploaded]))); + await loadWorkspaceTree(selectedBot.id, workspaceCurrentPath); + } + } catch (error: any) { + const msg = error?.response?.data?.detail || t.uploadFail; + notify(msg, { tone: 'error' }); + } finally { + setIsUploadingAttachments(false); + event.target.value = ''; + } + }; + + const onBaseProviderChange = (provider: string) => { + const preset = providerPresets[provider]; + setEditForm((p) => ({ + ...p, + llm_provider: provider, + llm_model: preset?.model || p.llm_model, + api_base: preset?.apiBase ?? p.api_base, + })); + setProviderTestResult(''); + }; + + const testProviderConnection = async () => { + if (!editForm.llm_provider || !editForm.llm_model || !editForm.api_key.trim()) { + notify(t.providerRequired, { tone: 'warning' }); + return; + } + setIsTestingProvider(true); + setProviderTestResult(''); + try { + const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, { + provider: editForm.llm_provider, + model: editForm.llm_model, + api_key: editForm.api_key.trim(), + api_base: editForm.api_base || undefined, + }); + if (res.data?.ok) { + const preview = (res.data.models_preview || []).slice(0, 3).join(', '); + setProviderTestResult(t.connOk(preview)); + } else { + setProviderTestResult(t.connFail(res.data?.detail || 'unknown error')); + } + } catch (error: any) { + const msg = error?.response?.data?.detail || error?.message || 'request failed'; + setProviderTestResult(t.connFail(msg)); + } finally { + setIsTestingProvider(false); + } + }; + + useEffect(() => { + if (!selectedBotId) { + setWorkspaceEntries([]); + setWorkspaceCurrentPath(''); + setWorkspaceParentPath(null); + setWorkspaceError(''); + setChannels([]); + setPendingAttachments([]); + setCronJobs([]); + setBotSkills([]); + setEnvParams({}); + return; + } + void loadWorkspaceTree(selectedBotId, ''); + void loadCronJobs(selectedBotId); + void loadBotSkills(selectedBotId); + void loadBotEnvParams(selectedBotId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedBotId]); + + useEffect(() => { + if (!workspaceAutoRefresh || !selectedBotId || selectedBot?.docker_status !== 'RUNNING') return; + let stopped = false; + + const tick = async () => { + if (stopped) return; + await loadWorkspaceTree(selectedBotId, workspaceCurrentPath); + }; + + void tick(); + const timer = window.setInterval(() => { + void tick(); + }, 2000); + + return () => { + stopped = true; + window.clearInterval(timer); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]); + + const saveBot = async (mode: 'params' | 'agent' | 'base') => { + if (!selectedBot) return; + setIsSaving(true); + try { + const payload: Record = {}; + if (mode === 'base') { + payload.name = editForm.name; + payload.llm_provider = editForm.llm_provider; + payload.llm_model = editForm.llm_model; + payload.api_base = editForm.api_base; + if (editForm.api_key.trim()) payload.api_key = editForm.api_key.trim(); + } + if (mode === 'params') { + payload.temperature = clampTemperature(Number(editForm.temperature)); + payload.top_p = Number(editForm.top_p); + payload.max_tokens = Number(editForm.max_tokens); + } + if (mode === 'agent') { + payload.agents_md = editForm.agents_md; + payload.soul_md = editForm.soul_md; + payload.user_md = editForm.user_md; + payload.tools_md = editForm.tools_md; + payload.identity_md = editForm.identity_md; + } + + await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}`, payload); + await refresh(); + setShowBaseModal(false); + setShowParamModal(false); + setShowAgentModal(false); + notify(t.configUpdated, { tone: 'success' }); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.saveFail; + notify(msg, { tone: 'error' }); + } finally { + setIsSaving(false); + } + }; + + const removeBot = async (botId?: string) => { + const targetId = botId || selectedBot?.id; + if (!targetId) return; + const ok = await confirm({ + title: t.delete, + message: t.deleteBotConfirm(targetId), + tone: 'warning', + }); + if (!ok) return; + try { + await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${targetId}`, { params: { delete_workspace: true } }); + await refresh(); + if (selectedBotId === targetId) setSelectedBotId(''); + notify(t.deleteBotDone, { tone: 'success' }); + } catch { + notify(t.deleteFail, { tone: 'error' }); + } + }; + + const clearConversationHistory = async () => { + if (!selectedBot) return; + const target = selectedBot.name || selectedBot.id; + const ok = await confirm({ + title: t.clearHistory, + message: t.clearHistoryConfirm(target), + tone: 'warning', + }); + if (!ok) return; + try { + await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/messages`); + setBotMessages(selectedBot.id, []); + notify(t.clearHistoryDone, { tone: 'success' }); + } catch (error: any) { + const msg = error?.response?.data?.detail || t.clearHistoryFail; + notify(msg, { tone: 'error' }); + } + }; + + const exportConversationJson = () => { + if (!selectedBot) return; + try { + const payload = { + bot_id: selectedBot.id, + bot_name: selectedBot.name || selectedBot.id, + exported_at: new Date().toISOString(), + message_count: conversation.length, + messages: conversation.map((m) => ({ + role: m.role, + text: m.text, + attachments: m.attachments || [], + kind: m.kind || 'final', + ts: m.ts, + datetime: new Date(m.ts).toISOString(), + })), + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `${selectedBot.id}-conversation-${stamp}.json`; + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch { + notify(t.exportHistoryFail, { tone: 'error' }); + } + }; + + const tabMap: Record = { + AGENTS: 'agents_md', + SOUL: 'soul_md', + USER: 'user_md', + TOOLS: 'tools_md', + IDENTITY: 'identity_md', + }; + + const renderWorkspaceNodes = (nodes: WorkspaceNode[]): ReactNode[] => { + const rendered: ReactNode[] = []; + if (workspaceParentPath !== null) { + rendered.push( + , + ); + } + + nodes.forEach((node) => { + const key = `${node.type}:${node.path}`; + if (node.type === 'dir') { + rendered.push( + , + ); + return; + } + + const previewable = isPreviewableWorkspaceFile(node); + const pdfFile = String(node.ext || '').toLowerCase() === '.pdf'; + rendered.push( + , + ); + }); + return rendered; + }; + + return ( + <> +
+ {!compactMode ? ( +
+
+

{t.titleBots}

+
+ + +
+
+ +
+ {bots.map((bot) => { + const selected = selectedBotId === bot.id; + const controlState = controlStateByBot[bot.id]; + const isOperating = operatingBotId === bot.id; + const isStarting = controlState === 'starting'; + const isStopping = controlState === 'stopping'; + return ( +
setSelectedBotId(bot.id)}> +
+ ); + })} +
+
+ ) : null} + +
+ {selectedBot ? ( +
+
+
+ {conversation.length === 0 ? ( +
+ {t.noConversation} +
+ ) : ( + conversationNodes + )} + + {isThinking ? ( +
+
+
+ Nanobot +
+
+
+ + + +
+
{t.thinking}
+
+
+
+ ) : null} + +
+
+ +
+ +

Mdo5qk; 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 VaniiJKt2Ef002ovPDHLkV1k*kol5`! literal 0 HcmV?d00001 diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/BotManagementView.tsx b/frontend/src/components/BotManagementView.tsx new file mode 100644 index 0000000..99eb800 --- /dev/null +++ b/frontend/src/components/BotManagementView.tsx @@ -0,0 +1 @@ +export { ManagementModule as BotManagementView } from '../modules/management/ManagementModule'; diff --git a/frontend/src/components/CreateBotModal.tsx b/frontend/src/components/CreateBotModal.tsx new file mode 100644 index 0000000..8d1ba26 --- /dev/null +++ b/frontend/src/components/CreateBotModal.tsx @@ -0,0 +1 @@ +export { CreateBotModal } from '../modules/management/components/CreateBotModal'; diff --git a/frontend/src/components/KernelManagerModal.tsx b/frontend/src/components/KernelManagerModal.tsx new file mode 100644 index 0000000..f6f7489 --- /dev/null +++ b/frontend/src/components/KernelManagerModal.tsx @@ -0,0 +1 @@ +export { KernelManagerModal } from '../modules/management/components/KernelManagerModal'; diff --git a/frontend/src/components/VisualDeckView.tsx b/frontend/src/components/VisualDeckView.tsx new file mode 100644 index 0000000..dcfc25f --- /dev/null +++ b/frontend/src/components/VisualDeckView.tsx @@ -0,0 +1 @@ +export { DashboardModule as VisualDeckView } from '../modules/dashboard/DashboardModule'; diff --git a/frontend/src/components/VoxelBot.tsx b/frontend/src/components/VoxelBot.tsx new file mode 100644 index 0000000..d7954e5 --- /dev/null +++ b/frontend/src/components/VoxelBot.tsx @@ -0,0 +1 @@ +export { VoxelBot } from '../modules/dashboard/components/VoxelBot'; diff --git a/frontend/src/components/WorkingDeck.tsx b/frontend/src/components/WorkingDeck.tsx new file mode 100644 index 0000000..21e9e20 --- /dev/null +++ b/frontend/src/components/WorkingDeck.tsx @@ -0,0 +1 @@ +export { WorkingDeck } from '../modules/dashboard/components/WorkingDeck'; diff --git a/frontend/src/components/lucent/LucentPromptProvider.tsx b/frontend/src/components/lucent/LucentPromptProvider.tsx new file mode 100644 index 0000000..eb0d1b8 --- /dev/null +++ b/frontend/src/components/lucent/LucentPromptProvider.tsx @@ -0,0 +1,168 @@ +import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'; +import { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from 'lucide-react'; +import { useAppStore } from '../../store/appStore'; +import './lucent-prompt.css'; + +type PromptTone = 'info' | 'success' | 'warning' | 'error'; + +interface NotifyOptions { + title?: string; + tone?: PromptTone; + durationMs?: number; +} + +interface ConfirmOptions { + title?: string; + message: string; + tone?: PromptTone; + confirmText?: string; + cancelText?: string; +} + +interface ToastItem { + id: number; + title?: string; + message: string; + tone: PromptTone; +} + +interface ConfirmState { + title?: string; + message: string; + tone: PromptTone; + confirmText: string; + cancelText: string; + resolve: (value: boolean) => void; +} + +interface LucentPromptApi { + notify: (message: string, options?: NotifyOptions) => void; + confirm: (options: ConfirmOptions) => Promise; +} + +const LucentPromptContext = createContext(null); + +function ToneIcon({ tone }: { tone: PromptTone }) { + if (tone === 'success') return ; + if (tone === 'warning') return ; + if (tone === 'error') return ; + return ; +} + +export function LucentPromptProvider({ children }: { children: ReactNode }) { + const locale = useAppStore((s) => s.locale); + const [toasts, setToasts] = useState([]); + const [confirmState, setConfirmState] = useState(null); + const idRef = useRef(1); + + const notify = useCallback((message: string, options?: NotifyOptions) => { + const text = String(message || '').trim(); + if (!text) return; + const id = idRef.current++; + const tone = options?.tone || 'info'; + setToasts((prev) => [...prev, { id, title: options?.title, message: text, tone }]); + const ttl = Math.max(1400, Math.min(options?.durationMs || 2600, 8000)); + window.setTimeout(() => { + setToasts((prev) => prev.filter((item) => item.id !== id)); + }, ttl); + }, []); + + const confirm = useCallback( + (options: ConfirmOptions) => + new Promise((resolve) => { + const message = String(options?.message || '').trim(); + if (!message) { + resolve(false); + return; + } + setConfirmState({ + title: options?.title, + message, + tone: options?.tone || 'warning', + confirmText: options?.confirmText || (locale === 'zh' ? '确定' : 'Confirm'), + cancelText: options?.cancelText || (locale === 'zh' ? '取消' : 'Cancel'), + resolve, + }); + }), + [locale], + ); + + const closeConfirm = useCallback( + (value: boolean) => { + setConfirmState((prev) => { + if (prev) prev.resolve(value); + return null; + }); + }, + [], + ); + + const value = useMemo( + () => ({ + notify, + confirm, + }), + [notify, confirm], + ); + + return ( + + {children} + +

Mdo5qk; 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 VaniiJKt2Ef002ovPDHLkV1k*kol5`! literal 0 HcmV?d00001 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..75d71cc --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,1048 @@ +:root, +.app-shell[data-theme='dark'] { + --bg: #050d1f; + --bg-2: #0b2248; + --panel: #0f203f; + --panel-soft: #142a52; + --line: #3d5685; + --text: #f2f7ff; + --title: #d7e5ff; + --subtitle: #c1d1ee; + --muted: #9ab1db; + --icon: #d9e8ff; + --icon-muted: #b9ccef; + --brand: #5b95ff; + --brand-soft: #284d8f; + --ok: #25c88a; + --warn: #e2b54a; + --err: #eb6a6a; + --shadow: 0 18px 45px rgba(0, 0, 0, 0.45); +} + +.app-shell[data-theme='light'] { + --bg: #edf2fb; + --bg-2: #d3e1ff; + --panel: #ffffff; + --panel-soft: #f3f7ff; + --line: #b7c7e6; + --text: #10244b; + --title: #0c2149; + --subtitle: #294675; + --muted: #456092; + --icon: #16386f; + --icon-muted: #385b91; + --brand: #2f69e2; + --brand-soft: #d7e6ff; + --ok: #11a56f; + --warn: #c68b14; + --err: #d14b4b; + --shadow: 0 10px 30px rgba(45, 77, 143, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + color: var(--text); + background-color: var(--bg); + background: + radial-gradient(1200px 800px at -12% -20%, var(--bg-2) 0%, transparent 60%), + radial-gradient(900px 600px at 110% 8%, var(--bg-2) 0%, transparent 62%), + linear-gradient(165deg, var(--bg) 0%, color-mix(in oklab, var(--bg) 75%, #000 25%) 100%); + font-family: 'Avenir Next', 'Segoe UI', 'PingFang SC', 'Noto Sans SC', sans-serif; +} + +.app-shell { + min-height: 100vh; + padding: 18px; +} + +.app-frame { + height: calc(100vh - 36px); + display: grid; + grid-template-rows: auto 1fr; + gap: 14px; +} + +.app-header { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + padding: 14px 16px; + box-shadow: var(--shadow); + backdrop-filter: blur(2px); +} + +.app-header-top { + align-items: flex-start; +} + +.app-title { + display: flex; + align-items: center; + gap: 10px; +} + +.app-title-icon { + width: 22px; + height: 22px; + object-fit: contain; +} + +.app-title svg { + color: var(--icon); + stroke-width: 2.2; +} + +.app-title h1 { + margin: 0; + font-size: 20px; + font-weight: 800; + color: var(--title); +} + +.app-title p { + margin: 2px 0 0; + color: var(--subtitle); + font-size: 12px; +} + +.global-switches { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.switch-compact { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 5px; + border-radius: 999px; + border: 1px solid var(--line); + background: var(--panel-soft); +} + +.switch-label { + font-size: 12px; + color: var(--muted); + margin-left: 6px; + margin-right: 2px; +} + +.switch-btn { + border: 1px solid transparent; + border-radius: 999px; + min-width: 34px; + height: 30px; + padding: 0 8px; + background: transparent; + color: var(--icon-muted); + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 700; +} + +.switch-btn svg { + color: currentColor; + stroke-width: 2.2; +} + +.switch-btn-lang { + min-width: 40px; + padding: 0 10px; + gap: 0; + font-size: 12px; +} + +.switch-btn.active { + border-color: color-mix(in oklab, var(--brand) 60%, var(--line) 40%); + background: color-mix(in oklab, var(--brand) 20%, transparent); + color: var(--icon); +} + +.main-stage { + min-height: 0; + height: 100%; +} + +.panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + padding: 14px; + box-shadow: var(--shadow); +} + +.panel h2 { + margin: 0; + font-size: 19px; + color: var(--title); +} + +.panel-desc { + margin: 4px 0 0; + color: var(--subtitle); + font-size: 13px; + font-weight: 600; +} + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + height: 100%; +} + +.grid-ops { + display: grid; + grid-template-columns: 320px 1fr 360px; + gap: 12px; + height: 100%; +} + +.grid-ops.grid-ops-compact { + grid-template-columns: minmax(0, 1fr) minmax(280px, 420px); +} + +.stack { + display: flex; + flex-direction: column; + gap: 10px; +} + +.input, +.select, +.textarea { + width: 100%; + border: 1px solid color-mix(in oklab, var(--line) 70%, var(--text) 8%); + background: var(--panel-soft); + color: var(--text); + border-radius: 10px; + padding: 9px 10px; + font-size: 13px; + outline: none; +} + +.app-shell[data-theme='light'] .input, +.app-shell[data-theme='light'] .select, +.app-shell[data-theme='light'] .textarea { + background: #f8fbff; +} + +.textarea { + resize: vertical; + min-height: 110px; +} + +.md-area { + min-height: 160px; + font-family: 'SF Mono', Menlo, Consolas, monospace; +} + +.field-label { + color: var(--subtitle); + font-size: 12px; +} + +.input:focus, +.select:focus, +.textarea:focus { + border-color: var(--brand); +} + +.input:disabled, +.select:disabled, +.textarea:disabled { + background: color-mix(in oklab, var(--panel-soft) 72%, var(--line) 28%); + color: var(--muted); + cursor: not-allowed; +} + +.btn { + border: 1px solid transparent; + border-radius: 10px; + padding: 9px 12px; + font-size: 13px; + font-weight: 700; + cursor: pointer; +} + +.btn-primary { + background: var(--brand); + color: #fff; +} + +.btn-primary svg { + color: #fff; +} + +.btn-primary:hover { + filter: brightness(1.08); +} + +.btn-secondary { + background: color-mix(in oklab, var(--panel-soft) 78%, var(--brand-soft) 22%); + border-color: var(--line); + color: var(--text); +} + +.btn-secondary svg { + color: var(--icon); + stroke-width: 2.1; +} + +.btn-success { + background: color-mix(in oklab, var(--ok) 20%, var(--panel-soft) 80%); + border-color: color-mix(in oklab, var(--ok) 46%, var(--line) 54%); + color: color-mix(in oklab, var(--text) 72%, white 28%); +} + +.btn-success svg { + color: currentColor; + stroke-width: 2.1; +} + +.btn-danger svg { + color: currentColor; + stroke-width: 2.1; +} + +.btn-danger { + background: color-mix(in oklab, var(--err) 18%, var(--panel-soft) 82%); + border-color: color-mix(in oklab, var(--err) 44%, var(--line) 56%); + color: color-mix(in oklab, var(--text) 70%, white 30%); +} + +.row-between { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.card { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel-soft); + padding: 10px; +} + +.app-shell[data-theme='light'] .card { + background: #f7faff; +} + +.selectable { + transition: border-color 0.2s; +} + +.selectable:hover { + border-color: color-mix(in oklab, var(--brand) 50%, var(--line) 50%); +} + +.badge { + display: inline-flex; + align-items: center; + padding: 3px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + border: 1px solid; +} + +.badge-ok { + color: color-mix(in oklab, var(--ok) 66%, white 34%); + background: color-mix(in oklab, var(--ok) 22%, transparent); + border-color: color-mix(in oklab, var(--ok) 52%, transparent); +} + +.badge-warn { + color: color-mix(in oklab, var(--warn) 60%, white 40%); + background: color-mix(in oklab, var(--warn) 16%, transparent); + border-color: color-mix(in oklab, var(--warn) 42%, transparent); +} + +.badge-err { + color: color-mix(in oklab, var(--err) 60%, white 40%); + background: color-mix(in oklab, var(--err) 16%, transparent); + border-color: color-mix(in oklab, var(--err) 42%, transparent); +} + +.badge-unknown { + color: var(--muted); + background: color-mix(in oklab, var(--line) 46%, transparent); + border-color: var(--line); +} + +.list-scroll { + overflow: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.table th, +.table td { + border-bottom: 1px solid var(--line); + text-align: left; + padding: 8px; +} + +.table th { + color: var(--subtitle); + font-size: 12px; + font-weight: 700; +} + +.mono { + font-family: 'SF Mono', Menlo, Consolas, monospace; +} + +.wizard-steps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-bottom: 12px; +} + +.wizard-shell { + background: + linear-gradient(160deg, color-mix(in oklab, var(--panel) 88%, var(--brand-soft) 12%) 0%, var(--panel) 100%); + min-height: 760px; +} + +.wizard-head { + padding: 2px 2px 4px; +} + +.wizard-steps-enhanced .wizard-step { + border-radius: 12px; + border-color: color-mix(in oklab, var(--line) 72%, transparent); + background: color-mix(in oklab, var(--panel-soft) 82%, transparent); + font-weight: 700; +} + +.wizard-steps-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.wizard-step { + border: 1px solid var(--line); + border-radius: 10px; + padding: 8px; + font-size: 12px; + color: var(--muted); + background: color-mix(in oklab, var(--panel-soft) 80%, black 20%); +} + +.wizard-step.active { + color: var(--text); + border-color: color-mix(in oklab, var(--brand) 65%, var(--line) 35%); + background: color-mix(in oklab, var(--brand-soft) 56%, var(--panel-soft) 44%); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--brand) 45%, transparent); +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.log-view { + background: color-mix(in oklab, var(--panel-soft) 82%, black 18%); + border: 1px solid var(--line); + border-radius: 10px; + padding: 8px; + height: 100%; + overflow: auto; + font-size: 12px; + font-family: 'SF Mono', Menlo, Consolas, monospace; +} + +.log-line { + padding: 5px 0; + border-bottom: 1px solid color-mix(in oklab, var(--line) 66%, transparent); + color: var(--text); +} + +.ops-chat-panel { + min-width: 0; +} + +.ops-header-band { + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px; + background: color-mix(in oklab, var(--panel-soft) 88%, var(--brand-soft) 12%); +} + +.ops-log-view { + min-height: 420px; +} + +.bot-card { + margin-bottom: 8px; + cursor: pointer; +} + +.bot-card.selected { + border-color: var(--brand); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--brand) 45%, transparent); +} + +.bot-name { + font-weight: 700; +} + +.bot-id, +.bot-meta { + color: color-mix(in oklab, var(--text) 70%, var(--muted) 30%); + font-size: 12px; + font-weight: 600; +} + +.telemetry-card { + display: grid; + gap: 7px; + font-size: 13px; +} + +.section-mini-title { + margin: 0; + font-size: 13px; + color: var(--subtitle); + font-weight: 700; +} + +.chat-tabs { + display: flex; + gap: 8px; +} + +.chat-view { + min-height: 420px; + max-height: 62vh; + overflow: auto; + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px; + background: var(--panel-soft); +} + +.chat-bubble { + margin-bottom: 8px; + padding: 8px 10px; + border-radius: 10px; + line-height: 1.45; + white-space: pre-wrap; + color: var(--text); + font-size: 14px; + font-weight: 600; +} + +.chat-bubble.assistant { + border: 1px solid color-mix(in oklab, var(--brand) 45%, var(--line) 55%); + background: color-mix(in oklab, var(--brand-soft) 36%, var(--panel-soft) 64%); +} + +.chat-bubble.user { + border: 1px solid color-mix(in oklab, var(--ok) 55%, var(--line) 45%); + background: color-mix(in oklab, var(--ok) 18%, var(--panel-soft) 82%); +} + +.chat-bubble.system { + border: 1px dashed color-mix(in oklab, var(--warn) 50%, var(--line) 50%); + color: var(--text); + background: color-mix(in oklab, var(--warn) 18%, var(--panel-soft) 82%); + font-size: 12px; + font-weight: 700; +} + +.telemetry-strong { + color: var(--text); +} + +.telemetry-strong .mono { + color: var(--text); +} + +.event-list { + display: grid; + gap: 8px; + max-height: 220px; + overflow: auto; +} + +.event-item { + display: grid; + grid-template-columns: 92px 1fr; + gap: 8px; + align-items: center; + padding: 6px 8px; + border: 1px solid var(--line); + border-radius: 8px; + background: color-mix(in oklab, var(--panel-soft) 90%, var(--panel) 10%); +} + +.event-state { + font-size: 11px; + font-weight: 700; +} + +.event-thinking .event-state { + color: #6ea5ff; +} + +.event-tool_call .event-state { + color: #67d3b1; +} + +.event-success .event-state { + color: #53cf95; +} + +.event-error .event-state { + color: #ef6666; +} + +.dialog-status-strip { + display: grid; + grid-template-columns: auto auto 1fr; + gap: 8px; + align-items: center; +} + +.state-chip { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid var(--line); + border-radius: 999px; + padding: 4px 10px; + font-size: 12px; + font-weight: 800; + color: var(--text); +} + +.state-running { + border-color: color-mix(in oklab, var(--ok) 60%, var(--line) 40%); + background: color-mix(in oklab, var(--ok) 22%, transparent); +} + +.state-active { + border-color: color-mix(in oklab, var(--brand) 65%, var(--line) 35%); + background: color-mix(in oklab, var(--brand) 18%, transparent); +} + +.state-last-action { + font-size: 13px; + color: var(--text); + font-weight: 700; +} + +.dialog-midstate-strip { + display: flex; + gap: 8px; + overflow: auto; +} + +.midstate-pill { + min-width: 240px; + border: 1px solid var(--line); + border-radius: 10px; + padding: 6px 8px; + display: grid; + gap: 4px; + color: var(--text); + background: var(--panel-soft); +} + +.midstate-pill .mono { + font-size: 11px; + font-weight: 800; +} + +.midstate-pill.state-thinking .mono { + color: #6ea5ff; +} + +.midstate-pill.state-tool_call .mono { + color: #67d3b1; +} + +.midstate-pill.state-success .mono { + color: #53cf95; +} + +.midstate-pill.state-error .mono { + color: #ef6666; +} + +.chat-meta-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.chat-role { + font-size: 11px; + font-weight: 800; + color: color-mix(in oklab, var(--text) 85%, var(--muted) 15%); +} + +.chat-time { + font-size: 10px; + color: var(--muted); +} + +.agent-tabs { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.agent-tabs-vertical { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 150px; +} + +.wizard-agent-layout { + display: grid; + grid-template-columns: 170px 1fr; + gap: 10px; + min-height: 420px; +} + +.agent-tab { + border: 1px solid var(--line); + background: var(--panel-soft); + color: var(--text); + border-radius: 8px; + padding: 6px 10px; + cursor: pointer; + font-size: 12px; +} + +.agent-tab.active { + border-color: var(--brand); + background: color-mix(in oklab, var(--brand-soft) 54%, var(--panel-soft) 46%); +} + +.modal-mask { + position: fixed; + inset: 0; + background: rgba(2, 9, 24, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 90; +} + +.modal-card { + width: min(680px, 90vw); + border: 1px solid var(--line); + border-radius: 14px; + background: var(--panel); + color: var(--text); + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35); +} + +.modal-wide { + width: min(980px, 94vw); +} + +.app-modal-mask { + z-index: 120; +} + +.app-modal-card { + width: min(1280px, 96vw); + max-height: 94vh; +} + +.app-modal-body { + min-height: 0; + max-height: calc(94vh - 92px); + overflow: auto; +} + +.modal-card h3 { + margin: 0; + color: var(--title); + font-size: 22px; + font-weight: 800; +} + +.modal-title-row { + display: grid; + gap: 4px; +} + +.modal-sub { + color: var(--subtitle); + font-size: 12px; + font-weight: 600; +} + +.slider-row { + display: grid; + gap: 6px; +} + +.wizard-step2-grid { + gap: 10px; +} + +.wizard-step2-card { + gap: 8px; + border-radius: 14px; + padding: 12px; + background: color-mix(in oklab, var(--panel-soft) 86%, var(--panel) 14%); +} + +.wizard-note-card { + border-style: dashed; +} + +.token-input-row { + align-items: start; +} + +.token-number-input { + max-width: 220px; +} + +.slider-row input[type='range'] { + width: 100%; +} + +.wizard-channel-list { + display: grid; + gap: 8px; +} + +.wizard-channel-card { + min-width: 0; + display: grid; + gap: 6px; +} + +.wizard-channel-compact { + padding: 10px; + border-radius: 10px; +} + +.wizard-dashboard-switches { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; +} + +.wizard-channel-summary { + display: grid; + gap: 8px; +} + +.wizard-icon-btn { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.icon-btn { + width: 34px; + height: 34px; + padding: 0; + border-radius: 9px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.factory-kpi-grid { + display: grid; + gap: 8px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.kpi-card { + border: 1px solid var(--line); + border-radius: 10px; + background: color-mix(in oklab, var(--panel-soft) 88%, transparent); + padding: 9px; +} + +.kpi-label { + font-size: 11px; + color: var(--subtitle); +} + +.kpi-value { + margin-top: 6px; + font-size: 22px; + font-weight: 800; +} + +/* Readability fallback rules (avoid low contrast when color-mix is limited). */ +.app-shell[data-theme='dark'] .panel { + background: #0f203f; + border-color: #3d5685; + color: #f2f7ff; +} + +.app-shell[data-theme='dark'] .card { + background: #142a52; + border-color: #3d5685; +} + +.app-shell[data-theme='dark'] .btn-secondary { + background: #1d3768; + border-color: #44639a; + color: #f2f7ff; +} + +.app-shell[data-theme='dark'] .btn-success { + background: #173f33; + border-color: #3ea486; + color: #d8fff2; +} + +.app-shell[data-theme='dark'] .btn-danger { + background: #3b1f2a; + border-color: #9d4f65; + color: #ffdce4; +} + +.app-shell[data-theme='light'] .panel { + background: #ffffff; + border-color: #b7c7e6; + color: #132a54; +} + +.app-shell[data-theme='light'] .card { + background: #f7faff; + border-color: #b7c7e6; + color: #132a54; +} + +.app-shell[data-theme='light'] .panel-desc, +.app-shell[data-theme='light'] .field-label, +.app-shell[data-theme='light'] .kicker, +.app-shell[data-theme='light'] .sub, +.app-shell[data-theme='light'] .chat-time, +.app-shell[data-theme='light'] .bot-id, +.app-shell[data-theme='light'] .bot-meta { + color: var(--subtitle); +} + +.app-shell[data-theme='light'] .btn-secondary { + background: #e9f1ff; + border-color: #b8cbee; + color: #163265; +} + +.app-shell[data-theme='light'] .btn-success { + background: #e6f8f1; + border-color: #9fd5bf; + color: #0f6b4e; +} + +.app-shell[data-theme='light'] .btn-danger { + background: #fdeeee; + border-color: #e0b0b0; + color: #8d2439; +} + +.app-shell[data-theme='light'] .input, +.app-shell[data-theme='light'] .select, +.app-shell[data-theme='light'] .textarea { + background: #ffffff; + border-color: #bccbe7; + color: #132a54; +} + +.app-shell[data-theme='light'] .input:disabled, +.app-shell[data-theme='light'] .select:disabled, +.app-shell[data-theme='light'] .textarea:disabled { + background: #edf2fb; + color: #5b6f97; +} + +.app-shell[data-theme='light'] .modal-card { + background: #ffffff; + border-color: #b7c7e6; + color: #132a54; +} + +.app-shell[data-theme='light'] .modal-card h3 { + color: var(--title); +} + +@media (max-width: 1400px) { + .grid-ops { + grid-template-columns: 280px 1fr 320px; + } + + .grid-ops.grid-ops-compact { + grid-template-columns: minmax(0, 1fr) minmax(260px, 360px); + } +} + +@media (max-width: 1160px) { + .grid-2, + .grid-ops, + .wizard-steps, + .wizard-steps-4, + .factory-kpi-grid, + .summary-grid, + .wizard-agent-layout { + grid-template-columns: 1fr; + } + + .app-frame { + height: auto; + min-height: calc(100vh - 36px); + } + + .app-shell { + padding: 12px; + } + + .app-header-top { + flex-direction: column; + align-items: flex-start; + } + + .global-switches { + width: 100%; + justify-content: flex-start; + } + + .wizard-shell { + min-height: 640px; + } +} + +@media (max-width: 980px) { + .grid-ops.grid-ops-compact { + grid-template-columns: 1fr; + grid-template-rows: auto auto; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..c5e091e --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,135 @@ +import { useMemo, useState } from 'react'; +import { Boxes, MoonStar, Sparkles, SunMedium } from 'lucide-react'; +import { + useAppStore, +} from './store/appStore'; +import { useBotsSync } from './hooks/useBotsSync'; +import { ImageFactoryModule } from './modules/images/ImageFactoryModule'; +import { BotWizardModule } from './modules/onboarding/BotWizardModule'; +import { BotDashboardModule } from './modules/dashboard/BotDashboardModule'; +import { pickLocale } from './i18n'; +import { appZhCn } from './i18n/app.zh-cn'; +import { appEn } from './i18n/app.en'; +import './App.css'; + +function App() { + const { theme, setTheme, locale, setLocale } = useAppStore(); + const [showImageFactory, setShowImageFactory] = useState(false); + const [showCreateWizard, setShowCreateWizard] = useState(false); + useBotsSync(); + const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn }); + const urlView = useMemo(() => { + const params = new URLSearchParams(window.location.search); + const forcedBotId = + (params.get('botId') || params.get('bot_id') || params.get('id') || '').trim(); + const compactRaw = (params.get('compact') || params.get('h5') || params.get('mobile') || '').trim().toLowerCase(); + const compactByFlag = ['1', 'true', 'yes', 'on'].includes(compactRaw); + const compactMode = compactByFlag || forcedBotId.length > 0; + return { forcedBotId, compactMode }; + }, []); + + return ( +