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) # Frontend package registry mirror (used by yarn, recommended in CN)
NPM_REGISTRY=https://registry.npmmirror.com NPM_REGISTRY=https://registry.npmmirror.com
# Optional DB override. # Database (choose one: SQLite / PostgreSQL / MySQL)
# Keep empty to use SQLite at: # SQLite example:
# sqlite:///{HOST_DATA_ROOT}/nanobot_dashboard.db # DATABASE_URL=sqlite:///${HOST_DATA_ROOT}/nanobot_dashboard.db
# PostgreSQL example:
# DATABASE_URL=postgresql+psycopg://user:password@127.0.0.1:5432/nanobot_dashboard # 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 DATABASE_URL=postgresql+psycopg://postgres:change_me@127.0.0.1:5432/nanobot_dashboard
# Redis cache # Redis cache (optional)
REDIS_ENABLED=true REDIS_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/8 REDIS_URL=redis://127.0.0.1:6379/8
REDIS_PREFIX=dashboard_nanobot REDIS_PREFIX=dashboard_nanobot
@ -40,7 +43,3 @@ PANEL_ACCESS_PASSWORD=change_me_panel_password
# Max upload size for backend validation (MB) # Max upload size for backend validation (MB)
UPLOAD_MAX_MB=100 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.example`
- 本地配置:`backend/.env` - 本地配置:`backend/.env`
- 关键项: - 关键项:
- `DATABASE_URL`:数据库连接串(支持 SQLite / PostgreSQL / MySQL - `DATABASE_URL`:数据库连接串(三选一:SQLite / PostgreSQL / MySQL
- `DATABASE_ECHO`SQL 日志输出开关 - `DATABASE_ECHO`SQL 日志输出开关
- 不提供自动数据迁移(如需升级迁移请离线完成后再切换连接串)
- `DATA_ROOT`、`BOTS_WORKSPACE_ROOT`:运行数据与 Bot 工作目录 - `DATA_ROOT`、`BOTS_WORKSPACE_ROOT`:运行数据与 Bot 工作目录
- `DEFAULT_*_MD`:创建向导默认模板来源(其中默认输出规范已并入 `DEFAULT_AGENTS_MD` - `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 # DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/nanobot_dashboard
# Show SQL statements in backend logs (debug only). # Show SQL statements in backend logs (debug only).
DATABASE_ECHO=true 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 cache
REDIS_ENABLED=false 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_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_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) 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() PANEL_ACCESS_PASSWORD: Final[str] = str(os.getenv("PANEL_ACCESS_PASSWORD") or "").strip()
DEFAULT_AGENTS_MD: Final[str] = _env_text( DEFAULT_AGENTS_MD: Final[str] = _env_text(

View File

@ -4,7 +4,6 @@ import mimetypes
import os import os
import re import re
import shutil import shutil
import sqlite3
import tempfile import tempfile
import zipfile import zipfile
from datetime import datetime from datetime import datetime
@ -20,10 +19,9 @@ from sqlmodel import Session, select
from core.config_manager import BotConfigManager from core.config_manager import BotConfigManager
from core.cache import cache 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.docker_manager import BotDockerManager
from core.settings import ( from core.settings import (
AUTO_MIGRATE_SQLITE_TO_PRIMARY,
BOTS_WORKSPACE_ROOT, BOTS_WORKSPACE_ROOT,
DATA_ROOT, DATA_ROOT,
DATABASE_ECHO, DATABASE_ECHO,
@ -39,7 +37,6 @@ from core.settings import (
REDIS_ENABLED, REDIS_ENABLED,
REDIS_PREFIX, REDIS_PREFIX,
REDIS_URL, REDIS_URL,
SQLITE_MIGRATION_SOURCE,
UPLOAD_MAX_MB, UPLOAD_MAX_MB,
) )
from models.bot import BotInstance, BotMessage, NanobotImage 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) return await call_next(request)
if request.url.path.startswith("/api/"): 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) return await call_next(request)
panel_error = _validate_panel_access_password(_get_supplied_panel_password_http(request)) panel_error = _validate_panel_access_password(_get_supplied_panel_password_http(request))
if panel_error: if panel_error:
@ -427,104 +429,6 @@ def _invalidate_images_cache() -> None:
cache.delete(_cache_key_images()) 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") @app.on_event("startup")
async def on_startup(): async def on_startup():
app.state.main_loop = asyncio.get_running_loop() 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"🧠 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'}") print(f"🔐 面板访问密码: {'enabled' if str(PANEL_ACCESS_PASSWORD or '').strip() else 'disabled'}")
init_database() init_database()
_migrate_sqlite_if_needed()
cache.delete_prefix("") cache.delete_prefix("")
with Session(engine) as session: with Session(engine) as session:
for bot in session.exec(select(BotInstance)).all(): for bot in session.exec(select(BotInstance)).all():

View File

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