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:
parent
97af8904ef
commit
24b9400767
1
.gitignore
vendored
1
.gitignore
vendored
@ -41,3 +41,4 @@ nul
|
||||
dataset/
|
||||
__pycache__/
|
||||
managerd.db
|
||||
assets-import.tar.gz
|
||||
|
||||
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}}
|
||||
|
||||
17
scripts/deploy/register-face-gallery.py
Normal file
17
scripts/deploy/register-face-gallery.py
Normal 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))
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user