diff --git a/.gitignore b/.gitignore index 1acd3ff..2443978 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ nul dataset/ __pycache__/ managerd.db +assets-import.tar.gz diff --git a/assets-import.tar.gz b/assets-import.tar.gz deleted file mode 100644 index e7544b3..0000000 Binary files a/assets-import.tar.gz and /dev/null differ diff --git a/cmd/safesightd/main.go b/cmd/safesightd/main.go index 84f583d..b2baa5a 100644 --- a/cmd/safesightd/main.go +++ b/cmd/safesightd/main.go @@ -42,15 +42,13 @@ func main() { taskRepo := storage.NewTasksRepo(store.DB()) assetsRepo := storage.NewAssetsRepo(store.DB()) modelsRepo := storage.NewModelsRepo(store.DB()) - if imported, err := service.ImportStandardTemplatesFromDir(assetsRepo, filepath.Join("templates", "standard_templates")); err != nil { - log.Fatalf("import standard templates: %v", err) - } else if imported > 0 { - log.Printf("imported %d standard templates", imported) - } - if imported, err := service.ImportStandardOverlaysFromDir(assetsRepo, filepath.Join("templates", "standard_overlays")); err != nil { - log.Fatalf("import standard overlays: %v", err) - } else if imported > 0 { - log.Printf("imported %d standard overlays", imported) + // Seed config assets on first run (when DB templates table is empty). + // Reads from configs/ directory bundled in the deployment package. + seedCount, seedErr := service.ImportSeedAssets(assetsRepo, "configs") + if seedErr != nil { + log.Printf("import seed assets: %v", seedErr) + } else if seedCount > 0 { + log.Printf("imported %d seed config assets", seedCount) } if err := service.EnsureDefaultIntegrations(assetsRepo); err != nil { log.Printf("ensure default integrations: %v", err) diff --git a/docs/deployment.md b/docs/deployment.md index 2a7b188..3deca8f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -9,46 +9,44 @@ cd safesight-control bash scripts/package.sh ``` -生成 `safesight-control-*.tar.gz`,包含 ARM64 和 AMD64 两个二进制、service 文件、配置示例。 +生成 `safesight-control-*.tar.gz`,包含: +- ARM64 + AMD64 二进制 +- service 文件、配置示例 +- **种子配置数据**(configs/:templates、profiles、overlays) +- **人脸库构建工具**(tools/:Python 脚本 + ONNX 模型) +- **离线 Python 依赖**(deps/:onnxruntime wheel) +- **辅助脚本**(scripts/:import-assets、register-face-gallery) --- -## 部署 - -### 部署到 Edge 设备(ARM64) +## 部署(ARM64) ```bash # 传到设备 -scp safesight-control-*.tar.gz orangepi@10.0.0.81:/tmp/ +scp safesight-control-*.tar.gz user@server:/tmp/ -# 在设备上 +# 解压 cd /tmp && tar -xzf safesight-control-*.tar.gz cd safesight-control-* -# 安装二进制 -sudo mkdir -p /opt/safesightd/bin +# 安装 +sudo mkdir -p /opt/safesightd/{bin,config,data} sudo cp safesightd-linux-arm64 /opt/safesightd/bin/safesightd sudo chmod +x /opt/safesightd/bin/safesightd - -# 创建数据和配置目录 -sudo mkdir -p /opt/safesightd/config /opt/safesightd/data sudo cp safesightd.json.example /opt/safesightd/config/safesightd.json -sudo vi /opt/safesightd/config/safesightd.json # 修改 agent_token +sudo cp -r configs /opt/safesightd/ +sudo cp -r tools /opt/safesightd/ +sudo cp -r scripts /opt/safesightd/ # 安装服务 sudo cp safesightd.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now safesightd + +# 首次启动自动导入种子数据,无需手动操作 ``` -### 部署到独立 Linux 服务器(AMD64) - -```bash -# 同上,二进制改为 safesightd-linux-amd64 -sudo cp safesightd-linux-amd64 /opt/safesightd/bin/safesightd -``` - -其余步骤相同。 +AMD64 部署同上,二进制改为 `safesightd-linux-amd64`。 --- @@ -73,6 +71,24 @@ sudo cp safesightd-linux-amd64 /opt/safesightd/bin/safesightd --- +## 人脸库构建(离线环境) + +```bash +# 1. 离线安装 Python 依赖(仅首次) +sudo pip3 install /opt/safesightd/deps/*.whl + +# 2. 通过 Web UI 导入照片后,触发构建 +curl -X POST http://localhost:18080/face-gallery/build + +# 3. 如从外部传入已构建的 face_gallery.db: +sudo mkdir -p /opt/safesightd/resources/standard_resources/face_gallery +sudo cp face_gallery.db /opt/safesightd/resources/standard_resources/face_gallery/ +sudo python3 /opt/safesightd/scripts/register-face-gallery.py +sudo systemctl restart safesightd +``` + +--- + ## 验证 ```bash diff --git a/internal/service/standard_templates.go b/internal/service/standard_templates.go index e3ec935..36001ad 100644 --- a/internal/service/standard_templates.go +++ b/internal/service/standard_templates.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "fmt" + "log" "os" "path/filepath" "strings" @@ -10,6 +11,91 @@ import ( "safesight-control/internal/storage" ) +// ImportSeedAssets imports all config assets (templates, profiles, overlays) from a +// seed configs directory on first run. Returns total import count. +// The seed directory is expected to have subdirectories: templates/, profiles/, overlays/. +func ImportSeedAssets(repo *storage.AssetsRepo, seedDir string) (int, error) { + if repo == nil { + return 0, nil + } + seedDir = filepath.Clean(strings.TrimSpace(seedDir)) + if seedDir == "" { + return 0, nil + } + if _, err := os.Stat(seedDir); os.IsNotExist(err) { + return 0, nil + } + + // Check if DB already has assets (not first run). + existing, err := repo.ListTemplates() + if err != nil { + return 0, err + } + if len(existing) > 0 { + return 0, nil + } + + total := 0 + for _, kind := range []string{"templates", "profiles", "overlays"} { + dir := filepath.Join(seedDir, kind) + files, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + continue + } + return total, err + } + for _, f := range files { + if f.IsDir() || !strings.HasSuffix(strings.ToLower(f.Name()), ".json") { + continue + } + path := filepath.Join(dir, f.Name()) + body, err := os.ReadFile(path) + if err != nil { + return total, fmt.Errorf("%s: %w", path, err) + } + var raw map[string]any + if err := json.Unmarshal(body, &raw); err != nil { + return total, fmt.Errorf("%s: %w", path, err) + } + if raw == nil { + raw = map[string]any{} + } + name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())))) + if name == "" { + log.Printf("skip %s: empty name", path) + continue + } + desc := stringValue(raw["description"]) + + switch kind { + case "templates": + if strings.TrimSpace(stringValue(raw["source"])) == "" { + raw["source"] = "standard" + } + b, _ := marshalConfigJSON(raw) + if err := repo.SaveTemplate(name, desc, string(b)); err != nil { + return total, fmt.Errorf("template %s: %w", name, err) + } + case "profiles": + tmplName := stringValue(raw["primary_template_name"]) + bizName := stringValue(raw["business_name"]) + b, _ := marshalConfigJSON(raw) + if err := repo.SaveProfile(name, tmplName, bizName, desc, string(b)); err != nil { + return total, fmt.Errorf("profile %s: %w", name, err) + } + case "overlays": + b, _ := marshalConfigJSON(raw) + if err := repo.SaveOverlay(name, desc, string(b)); err != nil { + return total, fmt.Errorf("overlay %s: %w", name, err) + } + } + total++ + } + } + return total, nil +} + func ImportStandardTemplatesFromDir(repo *storage.AssetsRepo, dir string) (int, error) { if repo == nil { return 0, fmt.Errorf("asset repository is not configured") diff --git a/internal/web/ui/templates/device.html b/internal/web/ui/templates/device.html index 9c17de5..63beac6 100644 --- a/internal/web/ui/templates/device.html +++ b/internal/web/ui/templates/device.html @@ -211,16 +211,22 @@
人脸库通过资源管理与基础配置维护
{{end}} + {{$fgConfig := false}} {{$fgSynced := true}} {{range $r := .StandardResources}}{{if eq $r.ResourceType "face_gallery"}} + {{$fgConfig = true}} {{$fgSynced = false}} {{range $d := $.DeviceResourceStatuses}}{{if and (eq $d.Name $r.Name) (eq $d.SHA256 $r.SHA256)}}{{$fgSynced = true}}{{end}}{{end}} {{end}}{{end}} + {{if $fgConfig}}
+ {{else}} +
人脸库尚未配置,请先在 资源管理 中添加 face_gallery 类型资源
+ {{end}}
diff --git a/internal/web/ui/templates/face_gallery.html b/internal/web/ui/templates/face_gallery.html index 7faa750..e40ccdf 100644 --- a/internal/web/ui/templates/face_gallery.html +++ b/internal/web/ui/templates/face_gallery.html @@ -16,6 +16,9 @@ +
+ +
{{if .FaceGalleryPersons}} diff --git a/scripts/deploy/register-face-gallery.py b/scripts/deploy/register-face-gallery.py new file mode 100644 index 0000000..6fcd4e2 --- /dev/null +++ b/scripts/deploy/register-face-gallery.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import sqlite3, hashlib, os + +db = '/opt/safesightd/data/app.db' +fg = '/opt/safesightd/resources/standard_resources/face_gallery/face_gallery.db' + +sha = hashlib.sha256(open(fg, 'rb').read()).hexdigest() +size = os.path.getsize(fg) + +conn = sqlite3.connect(db) +conn.execute( + "INSERT OR REPLACE INTO standard_resources(name,resource_type,version,sha256,size_bytes,description,file_path,created_at,updated_at) VALUES(?,?,?,?,?,?,?,datetime('now'),datetime('now'))", + ('face_gallery', 'face_gallery', 'v1', sha, size, '人脸库', 'resources/standard_resources/face_gallery/face_gallery.db'), +) +conn.commit() +conn.close() +print('OK sha256=' + sha + ' size=' + str(size)) diff --git a/scripts/package.sh b/scripts/package.sh index ffb6db7..b03c6b0 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -1,53 +1,115 @@ #!/bin/bash # 管理端离线部署包构建脚本 -# 交叉编译 safesightd,打包配置和部署脚本 +# 交叉编译 safesightd,打包配置、种子数据、工具和部署脚本 set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +EDGE_DIR="$(cd "$PROJECT_DIR/../safesight-edge" 2>/dev/null && pwd || echo '')" PACKAGE_NAME="safesight-control-$(date +%Y%m%d-%H%M%S)" BUILD_DIR="/tmp/$PACKAGE_NAME" echo "========== 构建管理端离线部署包 ==========" +echo "项目目录: $PROJECT_DIR" +[ -n "$EDGE_DIR" ] && echo "Edge 目录: $EDGE_DIR" echo "" -# 编译 -echo "[1/2] 交叉编译..." +# ── 编译 ── +echo "[1/6] 交叉编译..." cd "$PROJECT_DIR" mkdir -p "$BUILD_DIR" CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o "$BUILD_DIR/safesightd-linux-arm64" ./cmd/safesightd/ 2>/dev/null && echo " ✓ ARM64" || echo " ✗ ARM64 failed" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$BUILD_DIR/safesightd-linux-amd64" ./cmd/safesightd/ 2>/dev/null && echo " ✓ AMD64" || echo " ✗ AMD64 failed" -# 打包 -echo "[2/2] 打包..." -cp scripts/deploy/safesightd "$BUILD_DIR/" +# ── 部署文件 ── +echo "[2/6] 部署文件..." +mkdir -p "$BUILD_DIR/scripts" cp scripts/deploy/safesightd.service "$BUILD_DIR/" +cp scripts/deploy/safesightd "$BUILD_DIR/" 2>/dev/null || true +cp scripts/deploy/import-assets.py "$BUILD_DIR/scripts/" 2>/dev/null || true +cp scripts/deploy/register-face-gallery.py "$BUILD_DIR/scripts/" 2>/dev/null || true cp safesightd.json.example "$BUILD_DIR/safesightd.json.example" 2>/dev/null || true +echo " ✓ 部署脚本已复制" +# ── 种子配置数据 ── +echo "[3/6] 种子配置数据..." +SEED_SRC="" +if [ -n "$EDGE_DIR" ] && [ -d "$EDGE_DIR/configs" ]; then + SEED_SRC="$EDGE_DIR/configs" +elif [ -d "$PROJECT_DIR/seed-configs" ]; then + SEED_SRC="$PROJECT_DIR/seed-configs" +fi +if [ -n "$SEED_SRC" ]; then + mkdir -p "$BUILD_DIR/configs" + cp -r "$SEED_SRC/templates" "$BUILD_DIR/configs/" 2>/dev/null || true + cp -r "$SEED_SRC/profiles" "$BUILD_DIR/configs/" 2>/dev/null || true + cp -r "$SEED_SRC/overlays" "$BUILD_DIR/configs/" 2>/dev/null || true + echo " ✓ 种子数据 ($SEED_SRC)" +else + echo " ⚠ 找不到 configs 目录,跳过" +fi + +# ── 人脸库构建工具 ── +echo "[4/6] 人脸库构建工具..." +if [ -d "$PROJECT_DIR/tools" ]; then + mkdir -p "$BUILD_DIR/tools" + cp -r "$PROJECT_DIR/tools/gallery_builder" "$BUILD_DIR/tools/" 2>/dev/null || true + cp "$PROJECT_DIR/tools/build_gallery.py" "$BUILD_DIR/tools/" 2>/dev/null || true + mkdir -p "$BUILD_DIR/tools/models" + cp "$PROJECT_DIR/tools/models"/*.onnx "$BUILD_DIR/tools/models/" 2>/dev/null || true + cp "$PROJECT_DIR/tools/models"/*.json "$BUILD_DIR/tools/models/" 2>/dev/null || true + echo " ✓ 构建工具已复制" +else + echo " ⚠ 找不到 tools 目录,跳过" +fi + +# ── 离线 Python 依赖 ── +echo "[5/6] 离线 Python 依赖..." +mkdir -p "$BUILD_DIR/deps" +# 尝试下载 onnxruntime ARM64 wheel(离线安装用) +if command -v pip3 &>/dev/null; then + pip3 download onnxruntime numpy --platform linux_aarch64 --platform manylinux2014_aarch64 --only-binary=:all: -d "$BUILD_DIR/deps/" 2>/dev/null && echo " ✓ ARM64 wheels" || echo " ⚠ pip download 失败,跳过" +else + echo " ⚠ pip3 不可用,跳过" +fi + +# ── README ── +echo "[6/6] 生成 README..." cat > "$BUILD_DIR/README.txt" << 'EOF' SafeSight Control 离线部署包 ============================= -ARM64 设备部署: +快速部署(ARM64): + sudo mkdir -p /opt/safesightd/{bin,config,data,scripts,tools} sudo cp safesightd-linux-arm64 /opt/safesightd/bin/safesightd sudo chmod +x /opt/safesightd/bin/safesightd - sudo mkdir -p /opt/safesightd/config /opt/safesightd/data sudo cp safesightd.json.example /opt/safesightd/config/safesightd.json - sudo vi /opt/safesightd/config/safesightd.json # 修改 agent_token + sudo cp -r configs /opt/safesightd/ + sudo cp -r tools /opt/safesightd/ + sudo cp -r scripts /opt/safesightd/ sudo cp safesightd.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now safesightd -AMD64 设备部署: - 同上,二进制改为 safesightd-linux-amd64 +初始化(首次部署必做): + sudo python3 /opt/safesightd/scripts/import-assets.py /opt/safesightd/configs + +安装人脸库构建依赖(离线): + sudo pip3 install deps/*.whl + +人脸库注册(如已有 face_gallery.db): + sudo cp face_gallery.db /opt/safesightd/resources/standard_resources/face_gallery/ + sudo python3 /opt/safesightd/scripts/register-face-gallery.py 打开 http://:18080 EOF +# ── 打包 ── cd /tmp tar -czf "$PROJECT_DIR/$PACKAGE_NAME.tar.gz" "$PACKAGE_NAME" rm -rf "$BUILD_DIR" echo "" -echo "✓ 打包完成: $PROJECT_DIR/$PACKAGE_NAME.tar.gz" +echo "========== 打包完成 ==========" +echo "$PROJECT_DIR/$PACKAGE_NAME.tar.gz" ls -lh "$PROJECT_DIR/$PACKAGE_NAME.tar.gz"