Add system settings, one-click deploy, and simplify embed UI.
Remove Gate scout auto-seeding and the manual hub login button while keeping Gate login and instance SSO. Add password change and database backup/restore, plus deploy scripts with env auto-generation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""可选:从数据库中删除「Gate 扫单」分组及其下属服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app import create_app # noqa: E402
|
||||
from models import Service, ServiceGroup, db # noqa: E402
|
||||
|
||||
GROUP_NAMES = ("Gate 扫单",)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
removed_groups = 0
|
||||
removed_services = 0
|
||||
for name in GROUP_NAMES:
|
||||
group = ServiceGroup.query.filter_by(name=name).first()
|
||||
if not group:
|
||||
continue
|
||||
count = Service.query.filter_by(group_id=group.id).count()
|
||||
db.session.delete(group)
|
||||
removed_groups += 1
|
||||
removed_services += count
|
||||
if removed_groups:
|
||||
db.session.commit()
|
||||
print(
|
||||
f"已删除 {removed_groups} 个分组、{removed_services} 个服务。"
|
||||
" 请刷新导航首页。"
|
||||
)
|
||||
else:
|
||||
print("未找到「Gate 扫单」分组,无需清理。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location (Split-Path -Parent $PSScriptRoot)
|
||||
python scripts/deploy.py
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LocalNav 一键部署:环境检测、生成 .env、安装依赖。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ENV_EXAMPLE = ROOT / ".env.example"
|
||||
ENV_FILE = ROOT / ".env"
|
||||
VENV_DIR = ROOT / ".venv"
|
||||
REQUIREMENTS = ROOT / "requirements.txt"
|
||||
|
||||
MIN_PYTHON = (3, 10)
|
||||
|
||||
DEFAULTS = {
|
||||
"NAV_ADMIN_USERNAME": "admin",
|
||||
"NAV_ADMIN_PASSWORD": "admin123",
|
||||
"NAV_HUB_AUTO_LOGIN": "1",
|
||||
"NAV_COOKIES_INSECURE_HTTP": "1",
|
||||
}
|
||||
|
||||
|
||||
def _ok(msg: str) -> None:
|
||||
print(f"[OK] {msg}")
|
||||
|
||||
|
||||
def _warn(msg: str) -> None:
|
||||
print(f"[WARN] {msg}")
|
||||
|
||||
|
||||
def _fail(msg: str) -> None:
|
||||
print(f"[FAIL] {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_python() -> str:
|
||||
ver = sys.version_info
|
||||
if ver < MIN_PYTHON:
|
||||
_fail(f"需要 Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+,当前 {ver.major}.{ver.minor}")
|
||||
_ok(f"Python {ver.major}.{ver.minor}.{ver.micro}")
|
||||
return sys.executable
|
||||
|
||||
|
||||
def check_pip(py_exe: str) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
[py_exe, "-m", "pip", "--version"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
_ok("pip 可用")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
_fail("pip 不可用,请先安装 pip")
|
||||
|
||||
|
||||
def optional_tools() -> None:
|
||||
for name in ("git", "pm2"):
|
||||
if shutil.which(name):
|
||||
_ok(f"可选工具 {name} 已安装")
|
||||
else:
|
||||
_warn(f"未检测到 {name}(可选)")
|
||||
|
||||
|
||||
def read_env_lines(path: Path) -> list[str]:
|
||||
if not path.is_file():
|
||||
return []
|
||||
return path.read_text(encoding="utf-8-sig").splitlines()
|
||||
|
||||
|
||||
def parse_env_keys(lines: list[str]) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for line in lines:
|
||||
s = line.strip()
|
||||
if not s or s.startswith("#") or "=" not in s:
|
||||
continue
|
||||
key, _, val = s.partition("=")
|
||||
out[key.strip()] = val.strip()
|
||||
return out
|
||||
|
||||
|
||||
def write_env(lines: list[str], updates: dict[str, str]) -> None:
|
||||
existing = parse_env_keys(lines)
|
||||
merged = {**existing, **updates}
|
||||
out_lines: list[str] = []
|
||||
written: set[str] = set()
|
||||
|
||||
for line in lines:
|
||||
s = line.strip()
|
||||
if not s or s.startswith("#") or "=" not in s:
|
||||
out_lines.append(line)
|
||||
continue
|
||||
key, _, _ = s.partition("=")
|
||||
key = key.strip()
|
||||
if key in merged and key not in written:
|
||||
out_lines.append(f"{key}={merged[key]}")
|
||||
written.add(key)
|
||||
else:
|
||||
out_lines.append(line)
|
||||
|
||||
for key, val in merged.items():
|
||||
if key not in written:
|
||||
out_lines.append(f"{key}={val}")
|
||||
ENV_FILE.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def ensure_env() -> None:
|
||||
if not ENV_FILE.is_file():
|
||||
if ENV_EXAMPLE.is_file():
|
||||
shutil.copy2(ENV_EXAMPLE, ENV_FILE)
|
||||
_ok("已从 .env.example 创建 .env")
|
||||
else:
|
||||
ENV_FILE.write_text("", encoding="utf-8")
|
||||
_ok("已创建空 .env")
|
||||
|
||||
lines = read_env_lines(ENV_FILE)
|
||||
parsed = parse_env_keys(lines)
|
||||
updates: dict[str, str] = {}
|
||||
|
||||
secret = parsed.get("NAV_SECRET_KEY", "").strip()
|
||||
if not secret:
|
||||
updates["NAV_SECRET_KEY"] = secrets.token_hex(32)
|
||||
_ok("已自动生成 NAV_SECRET_KEY")
|
||||
|
||||
for key, val in DEFAULTS.items():
|
||||
if not parsed.get(key, "").strip():
|
||||
updates[key] = val
|
||||
|
||||
if updates:
|
||||
write_env(lines, updates)
|
||||
_ok(f"已写入 .env 配置项:{', '.join(updates.keys())}")
|
||||
else:
|
||||
_ok(".env 已完整,未覆盖现有配置")
|
||||
|
||||
|
||||
def venv_python() -> str:
|
||||
if platform.system() == "Windows":
|
||||
py = VENV_DIR / "Scripts" / "python.exe"
|
||||
else:
|
||||
py = VENV_DIR / "bin" / "python"
|
||||
return str(py)
|
||||
|
||||
|
||||
def ensure_venv(base_py: str) -> str:
|
||||
if not VENV_DIR.is_dir():
|
||||
subprocess.run([base_py, "-m", "venv", str(VENV_DIR)], check=True)
|
||||
_ok(f"已创建虚拟环境 {VENV_DIR}")
|
||||
else:
|
||||
_ok("虚拟环境已存在")
|
||||
return venv_python()
|
||||
|
||||
|
||||
def install_requirements(py_exe: str) -> None:
|
||||
cmd = [py_exe, "-m", "pip", "install", "-r", str(REQUIREMENTS)]
|
||||
try:
|
||||
subprocess.run(cmd, check=True, cwd=ROOT)
|
||||
except subprocess.CalledProcessError:
|
||||
_warn("默认 pip 源失败,尝试 https://pypi.org/simple")
|
||||
subprocess.run([*cmd, "-i", "https://pypi.org/simple"], check=True, cwd=ROOT)
|
||||
_ok("依赖安装完成")
|
||||
|
||||
|
||||
def smoke_test(py_exe: str) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
py_exe,
|
||||
"-c",
|
||||
"from app import create_app; create_app(); print('app import ok')",
|
||||
],
|
||||
check=True,
|
||||
cwd=ROOT,
|
||||
)
|
||||
_ok("应用启动检查通过")
|
||||
|
||||
|
||||
def print_summary() -> None:
|
||||
port = os.environ.get("NAV_PORT", "5070")
|
||||
print()
|
||||
print("=" * 50)
|
||||
print("部署完成")
|
||||
print(f" 访问地址: http://0.0.0.0:{port}")
|
||||
print(" 默认账号: admin / admin123")
|
||||
print(" 生产环境请尽快在「系统设置」中修改密码")
|
||||
print()
|
||||
print("启动方式(任选其一):")
|
||||
if platform.system() == "Windows":
|
||||
print(f" {venv_python()} app.py")
|
||||
else:
|
||||
print(f" {venv_python()} app.py")
|
||||
print(" pm2 start ecosystem.config.cjs")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(f"LocalNav 一键部署 · {platform.system()} {platform.release()}")
|
||||
print(f"项目目录: {ROOT}")
|
||||
print()
|
||||
|
||||
base_py = check_python()
|
||||
check_pip(base_py)
|
||||
optional_tools()
|
||||
ensure_env()
|
||||
py_exe = ensure_venv(base_py)
|
||||
install_requirements(py_exe)
|
||||
smoke_test(py_exe)
|
||||
print_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
python3 scripts/deploy.py
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""向 LocalNav SQLite 写入 gate_scout_order 两个服务(可重复执行,已存在则跳过)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
os.environ.setdefault("NAV_SEED_GATE_SCOUT", "1")
|
||||
|
||||
from app import app, db # noqa: E402
|
||||
from app import _ensure_gate_scout_services # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with app.app_context():
|
||||
_ensure_gate_scout_services()
|
||||
db.session.commit()
|
||||
print("完成。请刷新本地导航首页查看「Gate 扫单」分组。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user