v0.1.4
parent
c2fe3b8208
commit
84010e33ac
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
- 前端:
|
- 前端:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
111
backend/main.py
111
backend/main.py
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue