feat(control): 版本机制打通,UI 展示 agent/引擎/control 真实版本
- ui.go: 版本号由硬编码常量改为可注入变量( -X ...web.Version ) - 新增 service.FetchDeviceVersions: 调用 edge /v1/versions 采集 agent 版本/build_id/git_sha + media 引擎版本 + 设备模型数 - 设备详情页: 新增引擎版本/构建信息/模型数量展示 - 设备列表页: 新增版本列(含 build/git 悬停提示) - 构建脚本(3处): 注入 control 版本号(git describe) - 新增模板语法测试(43个模板防回归)
This commit is contained in:
parent
a1064961cc
commit
c6da0a6c6d
78
internal/service/device_versions.go
Normal file
78
internal/service/device_versions.go
Normal file
@ -0,0 +1,78 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"safesight-control/internal/models"
|
||||
)
|
||||
|
||||
// DeviceVersionInfo 汇总设备上 agent /v1/versions 返回的版本信息
|
||||
// (agent 构建注入的版本 + C++ media 引擎版本 + 设备上模型数量)
|
||||
type DeviceVersionInfo struct {
|
||||
AgentVersion string `json:"agent_version"`
|
||||
AgentBuildID string `json:"agent_build_id"`
|
||||
AgentBuildType string `json:"agent_build_type"`
|
||||
AgentGitSHA string `json:"agent_git_sha"`
|
||||
AgentBinary string `json:"agent_binary"`
|
||||
|
||||
MediaSupported bool `json:"media_supported"`
|
||||
MediaVersion string `json:"media_version"`
|
||||
MediaBinary string `json:"media_binary"`
|
||||
|
||||
ModelCount int `json:"model_count"`
|
||||
}
|
||||
|
||||
// FetchDeviceVersions 调用 edge agent 的 /v1/versions 接口,获取
|
||||
// agent / media 引擎 / 模型清单的版本信息。设备离线或接口不可用时返回 error。
|
||||
func FetchDeviceVersions(agent *AgentClient, device *models.Device) (*DeviceVersionInfo, error) {
|
||||
if agent == nil || device == nil || strings.TrimSpace(device.IP) == "" || device.AgentPort <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
body, status, err := agent.Do("GET", device.IP, device.AgentPort, "/v1/versions", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != 200 {
|
||||
return nil, fmt.Errorf("agent returned status %d", status)
|
||||
}
|
||||
var raw struct {
|
||||
Agent map[string]any `json:"agent"`
|
||||
MediaServer map[string]any `json:"media_server"`
|
||||
Models struct {
|
||||
Count int `json:"count"`
|
||||
} `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &DeviceVersionInfo{}
|
||||
if v, ok := raw.Agent["version"].(string); ok {
|
||||
info.AgentVersion = v
|
||||
}
|
||||
if v, ok := raw.Agent["build_id"].(string); ok {
|
||||
info.AgentBuildID = v
|
||||
}
|
||||
if v, ok := raw.Agent["build_type"].(string); ok {
|
||||
info.AgentBuildType = v
|
||||
}
|
||||
if v, ok := raw.Agent["git_sha"].(string); ok {
|
||||
info.AgentGitSHA = v
|
||||
}
|
||||
if v, ok := raw.Agent["binary"].(string); ok {
|
||||
info.AgentBinary = v
|
||||
}
|
||||
if v, ok := raw.MediaServer["supported"].(bool); ok {
|
||||
info.MediaSupported = v
|
||||
}
|
||||
if v, ok := raw.MediaServer["version"].(string); ok {
|
||||
info.MediaVersion = v
|
||||
}
|
||||
if v, ok := raw.MediaServer["binary"].(string); ok {
|
||||
info.MediaBinary = v
|
||||
}
|
||||
info.ModelCount = raw.Models.Count
|
||||
return info, nil
|
||||
}
|
||||
70
internal/web/template_parse_test.go
Normal file
70
internal/web/template_parse_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed ui/templates/*.html
|
||||
var testTemplateFS embed.FS
|
||||
|
||||
// 与 ui.go 相同的函数名集合(dummy 实现,仅用于语法验证)
|
||||
var testFuncs = template.FuncMap{
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"ago": func(v any) string { return "" },
|
||||
"alarmChannelSource": func(v any) any { return v },
|
||||
"auditActionLabel": func(v any) any { return v },
|
||||
"auditField": func(v any) any { return v },
|
||||
"auditStatusLabel": func(v any) any { return v },
|
||||
"displayDeviceName": func(v ...any) string { return "" },
|
||||
"displayDeviceTechnicalName": func(v ...any) any { return "" },
|
||||
"div": func(a, b int) int { return a / b },
|
||||
"formatTime": func(v any) any { return v },
|
||||
"formatTuneValue": func(v, s float64) string { return "" },
|
||||
"hasString": func(a []string, b string) bool { return false },
|
||||
"icon": func(name string) string { return "" },
|
||||
"inputBindingRef": func(v any) any { return v },
|
||||
"json": func(v any) any { return v },
|
||||
"loop": func(n int) []int { return nil },
|
||||
"modelTypeLabel": func(v any) any { return v },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"outputBindingValue": func(v ...any) any { return "" },
|
||||
"rawHTML": func(v any) any { return v },
|
||||
"resourceTypeLabel": func(v any) any { return v },
|
||||
"ruleLabel": func(v any) any { return v },
|
||||
"serviceBindingRef": func(v any) any { return v },
|
||||
"severityClass": func(v any) any { return v },
|
||||
"severityLabel": func(v any) any { return v },
|
||||
"shortHash": func(v any) any { return v },
|
||||
"shortID": func(v any) any { return v },
|
||||
"slotTypeLabel": func(v any) any { return v },
|
||||
"statusClass": func(v any) any { return v },
|
||||
"statusLabel": func(v any) any { return v },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"taskActionLabel": func(v any) any { return v },
|
||||
"taskGroupClass": func(v any) any { return v },
|
||||
"taskGroupLabel": func(v any) any { return v },
|
||||
"taskStatusClass": func(v any) any { return v },
|
||||
"taskStatusLabel": func(v any) any { return v },
|
||||
}
|
||||
|
||||
func TestTemplateSyntaxAll(t *testing.T) {
|
||||
entries, err := testTemplateFS.ReadDir("ui/templates")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := "ui/templates/" + e.Name()
|
||||
data, err := testTemplateFS.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("读取 %s: %v", name, err)
|
||||
}
|
||||
tmpl := template.New(e.Name()).Funcs(testFuncs)
|
||||
if _, err := tmpl.Parse(string(data)); err != nil {
|
||||
t.Fatalf("模板语法错误 %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("全部 %d 个模板语法通过\n", len(entries))
|
||||
}
|
||||
@ -56,7 +56,8 @@ const (
|
||||
deviceAssignmentPreviewDeviceCount = 8
|
||||
)
|
||||
|
||||
const version = "1.0"
|
||||
// control 端版本号,构建时通过 -ldflags "-X safesight-control/internal/web.Version=<版本>" 注入
|
||||
var Version = "dev"
|
||||
|
||||
type PageData struct {
|
||||
Title string
|
||||
@ -134,6 +135,8 @@ type PageData struct {
|
||||
FaceGalleryQuality map[string]string
|
||||
DeviceModelStatuses []service.InstalledModelStatus
|
||||
DeviceResourceStatuses []service.InstalledResourceStatus
|
||||
DeviceVersion *service.DeviceVersionInfo
|
||||
DeviceVersionErr string
|
||||
Templates []service.Template
|
||||
Template *service.Template
|
||||
AssetTab string
|
||||
@ -901,7 +904,7 @@ func (u *UI) Routes() (chi.Router, error) {
|
||||
}
|
||||
|
||||
func (u *UI) render(w http.ResponseWriter, r *http.Request, content string, data PageData) {
|
||||
data.Version = version
|
||||
data.Version = Version
|
||||
data.Year = time.Now().Year()
|
||||
if u.cfg != nil {
|
||||
if data.SystemCompanyName == "" {
|
||||
@ -2008,6 +2011,11 @@ func (u *UI) deviceDetailPageData(dev *models.Device) PageData {
|
||||
}
|
||||
// Load device model and resource status vs management standards.
|
||||
if u.agent != nil {
|
||||
if info, err := service.FetchDeviceVersions(u.agent, dev); err == nil {
|
||||
data.DeviceVersion = info
|
||||
} else {
|
||||
data.DeviceVersionErr = err.Error()
|
||||
}
|
||||
if items, err := service.FetchInstalledModelStatuses(u.agent, dev); err == nil {
|
||||
data.DeviceModelStatuses = items
|
||||
}
|
||||
|
||||
@ -54,7 +54,14 @@
|
||||
<div><span>管理地址</span><strong class="mono">{{.Device.IP}}:{{.Device.AgentPort}}</strong></div>
|
||||
<div><span>视频端口</span><strong class="mono">{{.Device.MediaPort}}</strong></div>
|
||||
<div><span>最后心跳</span><strong>{{ago .Device.LastSeenMs}}</strong></div>
|
||||
<div><span>版本</span><strong class="mono">{{if .Device.Version}}{{.Device.Version}}{{else}}-{{end}}</strong></div>
|
||||
<div><span>Agent 版本</span><strong class="mono">{{if .Device.Version}}{{.Device.Version}}{{else}}-{{end}}</strong></div>
|
||||
{{if .DeviceVersion}}
|
||||
<div><span>引擎版本</span><strong class="mono">{{if .DeviceVersion.MediaSupported}}{{if .DeviceVersion.MediaVersion}}{{.DeviceVersion.MediaVersion}}{{else}}运行中(版本未知){{end}}{{else}}未启用{{end}}</strong></div>
|
||||
<div><span>构建信息</span><strong class="mono" style="font-size:12px">{{if .DeviceVersion.AgentBuildID}}{{.DeviceVersion.AgentBuildID}}{{else}}-{{end}}{{if .DeviceVersion.AgentGitSHA}} <span class="muted">({{.DeviceVersion.AgentGitSHA}})</span>{{end}}</strong></div>
|
||||
<div><span>模型</span><strong class="mono">{{.DeviceVersion.ModelCount}} 个</strong></div>
|
||||
{{else if .DeviceVersionErr}}
|
||||
<div><span>引擎版本</span><strong class="mono" style="color:var(--danger)">获取失败</strong></div>
|
||||
{{end}}
|
||||
<div><span>当前场景模板</span><strong>{{if and .ConfigStatus .ConfigStatus.Metadata.Profile}}{{.ConfigStatus.Metadata.Profile}}{{else if .PersistedConfig}}{{.PersistedConfig.ProfileName}}{{else}}-{{end}}</strong></div>
|
||||
<div><span>通道名</span><strong>{{if and .ConfigStatus .ConfigStatus.Metadata.InstanceName}}{{.ConfigStatus.Metadata.InstanceName}}{{else if .Device.InstanceName}}{{.Device.InstanceName}}{{else}}-{{end}}</strong></div>
|
||||
</div>
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
<th>设备</th>
|
||||
<th>状态</th>
|
||||
<th>配置模板</th>
|
||||
<th>版本</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -112,6 +113,11 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mono small" {{if .Device.BuildID}}title="build {{.Device.BuildID}}{{if .Device.GitSha}} · {{.Device.GitSha}}{{end}}"{{end}}>
|
||||
{{if .Device.Version}}{{.Device.Version}}{{else}}-{{end}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a class="btn ghost" href="/devices/{{.Device.DeviceID}}">{{icon "detail"}}<span>详情</span></a>
|
||||
@ -120,7 +126,7 @@
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<div class="empty-title">还没有设备</div>
|
||||
<div class="muted">当前后台还没有发现或录入任何设备。</div>
|
||||
|
||||
@ -4,5 +4,6 @@ set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o safesightd-linux-arm64 ./cmd/safesightd/
|
||||
echo "✓ $(ls -lh safesightd-linux-arm64 | awk '{print $5,$NF}')"
|
||||
CTL_VERSION="$(git describe --tags --always 2>/dev/null || echo "dev")"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X safesight-control/internal/web.Version=${CTL_VERSION}" -o safesightd-linux-arm64 ./cmd/safesightd/
|
||||
echo "✓ $(ls -lh safesightd-linux-arm64 | awk '{print $5,$NF}') (version ${CTL_VERSION})"
|
||||
|
||||
@ -50,7 +50,9 @@ echo "[3/7] 编译项目..."
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo " 编译 Linux AMD64 版本..."
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$PACKAGE_DIR/bin/safesightd" ./cmd/safesightd
|
||||
CTL_VERSION="$(git describe --tags --always 2>/dev/null || echo "dev")"
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X safesight-control/internal/web.Version=${CTL_VERSION}" -o "$PACKAGE_DIR/bin/safesightd" ./cmd/safesightd
|
||||
echo " control 版本: ${CTL_VERSION}"
|
||||
|
||||
echo " 验证编译结果..."
|
||||
if [ ! -f "$PACKAGE_DIR/bin/safesightd" ]; then
|
||||
|
||||
@ -18,8 +18,11 @@ echo ""
|
||||
echo "[1/7] 交叉编译..."
|
||||
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"
|
||||
CTL_VERSION="$(git describe --tags --always 2>/dev/null || echo "dev")"
|
||||
LDFLAGS="-s -w -X safesight-control/internal/web.Version=${CTL_VERSION}"
|
||||
echo " control 版本: ${CTL_VERSION}"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -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="${LDFLAGS}" -o "$BUILD_DIR/safesightd-linux-amd64" ./cmd/safesightd/ 2>/dev/null && echo " ✓ AMD64" || echo " ✗ AMD64 failed"
|
||||
|
||||
# ── 部署文件 ──
|
||||
echo "[2/7] 部署文件..."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user