main
mula.liu 2026-03-10 12:40:01 +08:00
parent c2fe3b8208
commit 84010e33ac
6 changed files with 16 additions and 123 deletions

View File

@ -23,13 +23,16 @@ PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
# Frontend package registry mirror (used by yarn, 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 (choose one: SQLite / PostgreSQL / MySQL)
# SQLite example:
# DATABASE_URL=sqlite:///${HOST_DATA_ROOT}/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
DATABASE_URL=postgresql+psycopg://postgres:change_me@127.0.0.1:5432/nanobot_dashboard
# Redis cache
# Redis cache (optional)
REDIS_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/8
REDIS_PREFIX=dashboard_nanobot
@ -40,7 +43,3 @@ PANEL_ACCESS_PASSWORD=change_me_panel_password
# Max upload size for backend validation (MB)
UPLOAD_MAX_MB=100
# 升级迁移
AUTO_MIGRATE_SQLITE_TO_PRIMARY=true
SQLITE_MIGRATION_SOURCE=../data/nanobot_dashboard.db

View File

@ -62,8 +62,9 @@ graph TD
- 示例文件:`backend/.env.example`
- 本地配置:`backend/.env`
- 关键项:
- `DATABASE_URL`:数据库连接串(支持 SQLite / PostgreSQL / MySQL
- `DATABASE_URL`:数据库连接串(三选一:SQLite / PostgreSQL / MySQL
- `DATABASE_ECHO`SQL 日志输出开关
- 不提供自动数据迁移(如需升级迁移请离线完成后再切换连接串)
- `DATA_ROOT`、`BOTS_WORKSPACE_ROOT`:运行数据与 Bot 工作目录
- `DEFAULT_*_MD`:创建向导默认模板来源(其中默认输出规范已并入 `DEFAULT_AGENTS_MD`
- 前端:

View File

@ -12,10 +12,6 @@ BOTS_WORKSPACE_ROOT=../workspace/bots
# DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/nanobot_dashboard
# Show SQL statements in backend logs (debug only).
DATABASE_ECHO=true
# Auto-migrate legacy SQLite data into primary database on first startup.
AUTO_MIGRATE_SQLITE_TO_PRIMARY=true
# Legacy SQLite source path used by the auto-migrator.
SQLITE_MIGRATION_SOURCE=../data/nanobot_dashboard.db
# Redis cache
REDIS_ENABLED=false

View File

@ -122,10 +122,6 @@ REDIS_ENABLED: Final[bool] = _env_bool("REDIS_ENABLED", False)
REDIS_URL: Final[str] = str(os.getenv("REDIS_URL") or "").strip()
REDIS_PREFIX: Final[str] = str(os.getenv("REDIS_PREFIX") or "dashboard_nanobot").strip() or "dashboard_nanobot"
REDIS_DEFAULT_TTL: Final[int] = _env_int("REDIS_DEFAULT_TTL", 60, 1, 86400)
AUTO_MIGRATE_SQLITE_TO_PRIMARY: Final[bool] = _env_bool("AUTO_MIGRATE_SQLITE_TO_PRIMARY", True)
SQLITE_MIGRATION_SOURCE: Final[str] = _normalize_dir_path(
os.getenv("SQLITE_MIGRATION_SOURCE", str(Path(DATA_ROOT) / "nanobot_dashboard.db"))
)
PANEL_ACCESS_PASSWORD: Final[str] = str(os.getenv("PANEL_ACCESS_PASSWORD") or "").strip()
DEFAULT_AGENTS_MD: Final[str] = _env_text(

View File

@ -4,7 +4,6 @@ import mimetypes
import os
import re
import shutil
import sqlite3
import tempfile
import zipfile
from datetime import datetime
@ -20,10 +19,9 @@ from sqlmodel import Session, select
from core.config_manager import BotConfigManager
from core.cache import cache
from core.database import align_postgres_sequences, engine, get_session, init_database
from core.database import engine, get_session, init_database
from core.docker_manager import BotDockerManager
from core.settings import (
AUTO_MIGRATE_SQLITE_TO_PRIMARY,
BOTS_WORKSPACE_ROOT,
DATA_ROOT,
DATABASE_ECHO,
@ -39,7 +37,6 @@ from core.settings import (
REDIS_ENABLED,
REDIS_PREFIX,
REDIS_URL,
SQLITE_MIGRATION_SOURCE,
UPLOAD_MAX_MB,
)
from models.bot import BotInstance, BotMessage, NanobotImage
@ -349,7 +346,12 @@ async def bot_access_password_guard(request: Request, call_next):
return await call_next(request)
if request.url.path.startswith("/api/"):
if request.url.path in {"/api/panel/auth/status", "/api/panel/auth/login"}:
if request.url.path in {
"/api/panel/auth/status",
"/api/panel/auth/login",
"/api/health",
"/api/health/cache",
}:
return await call_next(request)
panel_error = _validate_panel_access_password(_get_supplied_panel_password_http(request))
if panel_error:
@ -427,104 +429,6 @@ def _invalidate_images_cache() -> None:
cache.delete(_cache_key_images())
def _parse_dt(raw: Any) -> datetime:
if isinstance(raw, datetime):
return raw
text = str(raw or "").strip()
if not text:
return datetime.utcnow()
for candidate in (text, text.replace("Z", "+00:00")):
try:
return datetime.fromisoformat(candidate)
except Exception:
continue
return datetime.utcnow()
def _target_has_data() -> bool:
with Session(engine) as session:
if session.exec(select(BotInstance).limit(1)).first() is not None:
return True
if session.exec(select(BotMessage).limit(1)).first() is not None:
return True
if session.exec(select(NanobotImage).limit(1)).first() is not None:
return True
return False
def _migrate_sqlite_if_needed() -> None:
if DATABASE_ENGINE == "sqlite":
return
if not AUTO_MIGRATE_SQLITE_TO_PRIMARY:
return
source_path = str(SQLITE_MIGRATION_SOURCE or "").strip()
if not source_path or not os.path.isfile(source_path):
return
if _target_has_data():
return
conn = sqlite3.connect(source_path)
conn.row_factory = sqlite3.Row
try:
with Session(engine) as session:
for row in conn.execute("SELECT * FROM botinstance"):
bot_id = str(row["id"])
if session.get(BotInstance, bot_id):
continue
session.add(
BotInstance(
id=bot_id,
name=str(row["name"] or ""),
access_password=str(row["access_password"] or ""),
workspace_dir=str(row["workspace_dir"] or ""),
docker_status=str(row["docker_status"] or "STOPPED"),
current_state=str(row["current_state"] or "IDLE"),
last_action=str(row["last_action"] or "") or None,
image_tag=str(row["image_tag"] or "nanobot-base:v0.1.4"),
created_at=_parse_dt(row["created_at"]),
updated_at=_parse_dt(row["updated_at"]),
)
)
for row in conn.execute("SELECT * FROM nanobotimage"):
tag = str(row["tag"])
if session.get(NanobotImage, tag):
continue
session.add(
NanobotImage(
tag=tag,
image_id=str(row["image_id"] or "") or None,
version=str(row["version"] or ""),
status=str(row["status"] or "READY"),
source_dir=str(row["source_dir"] or "") or None,
created_at=_parse_dt(row["created_at"]),
)
)
session.commit()
for row in conn.execute("SELECT * FROM botmessage ORDER BY id ASC"):
message_id = int(row["id"])
if session.get(BotMessage, message_id):
continue
session.add(
BotMessage(
id=message_id,
bot_id=str(row["bot_id"] or ""),
role=str(row["role"] or "assistant"),
text=str(row["text"] or ""),
media_json=str(row["media_json"] or "") or None,
feedback=str(row["feedback"] or "") or None,
feedback_at=_parse_dt(row["feedback_at"]) if row["feedback_at"] else None,
created_at=_parse_dt(row["created_at"]),
)
)
session.commit()
align_postgres_sequences()
finally:
conn.close()
@app.on_event("startup")
async def on_startup():
app.state.main_loop = asyncio.get_running_loop()
@ -534,7 +438,6 @@ async def on_startup():
print(f"🧠 Redis 缓存: {'enabled' if cache.ping() else 'disabled'} ({REDIS_URL if REDIS_ENABLED else 'not configured'})")
print(f"🔐 面板访问密码: {'enabled' if str(PANEL_ACCESS_PASSWORD or '').strip() else 'disabled'}")
init_database()
_migrate_sqlite_if_needed()
cache.delete_prefix("")
with Session(engine) as session:
for bot in session.exec(select(BotInstance)).all():

View File

@ -24,8 +24,6 @@ services:
REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot}
REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-60}
PANEL_ACCESS_PASSWORD: ${PANEL_ACCESS_PASSWORD:-}
AUTO_MIGRATE_SQLITE_TO_PRIMARY: ${AUTO_MIGRATE_SQLITE_TO_PRIMARY:-true}
SQLITE_MIGRATION_SOURCE: ${SQLITE_MIGRATION_SOURCE:-}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${HOST_DATA_ROOT}:${HOST_DATA_ROOT}