feat: 离线部署包一体化,启动自动导入种子数据,人脸库构建流程修复

- package.sh: 打包包含 configs 种子数据、tools 构建工具、deps 离线依赖、scripts 辅助脚本
- ImportSeedAssets: 首次启动检测空库自动导入 templates/profiles/overlays
- face_gallery.html: 添加构建人脸库按钮
- device.html: 无标准配置时显示设备实际模型/资源,无人脸库配置时隐藏按钮
- register-face-gallery.py: 手动注册已构建人脸库到 standard_resources
- deployment.md: 更新部署流程反映新包结构
- .gitignore: 排除 *.tar.gz 构建产物
This commit is contained in:
tian 2026-07-23 17:57:47 +08:00
parent 97af8904ef
commit 24b9400767
9 changed files with 230 additions and 41 deletions

1
.gitignore vendored
View File

@ -41,3 +41,4 @@ nul
dataset/
__pycache__/
managerd.db
assets-import.tar.gz

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -211,16 +211,22 @@
<div><span>人脸库</span><strong>通过资源管理与基础配置维护</strong></div>
</div>
{{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}}
<div class="actions service-actions-row">
<form method="post" action="/devices/{{.Device.DeviceID}}/face-gallery/reload">
<button type="submit" class="{{if $fgSynced}}secondary{{else}}primary{{end}}">{{if $fgSynced}}重载人脸库{{else}}⚠ 重载人脸库(待同步){{end}}</button>
</form>
</div>
{{else}}
<div class="muted small" style="margin-top:8px">人脸库尚未配置,请先在 <a href="/resources">资源管理</a> 中添加 face_gallery 类型资源</div>
{{end}}
</section>
<section class="panel-block" id="device-observability">

View File

@ -16,6 +16,9 @@
<input type="search" id="person-search" class="input" placeholder="搜索人员..." style="width:180px" oninput="filterPersons()">
<button class="btn secondary" type="button" onclick="showImport()">批量导入</button>
<button class="btn" type="button" onclick="showAdd()">新增人员</button>
<form method="post" action="/face-gallery/build" style="display:inline" onsubmit="return confirm('确认从已导入的照片构建人脸库?')">
<button class="btn primary" type="submit">构建人脸库</button>
</form>
</div>
</div>
{{if .FaceGalleryPersons}}

View File

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

View File

@ -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://<IP>: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"