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 @@