feat(v2): T-309 SQLite fall-event persistence (mattn/go-sqlite3)

Persist confirmed fall events to a local SQLite DB under artifacts/:
camera host/port/channel + event id/track/source/config version/state/
latency/screenshot path/confirmed-at. Schema stores no password (guarded
by test). Wire openRecorder/saveRecord into the alerts loop; failures are
non-fatal. Re-applied the SILVER-POSE vendored os_windows.go patch that
go mod vendor reverted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 22:15:55 +08:00
co-authored by Claude Opus 4.8
parent 4b7dfc2db6
commit 6c779aafb4
62 changed files with 292138 additions and 32 deletions
+27 -32
View File
@@ -34,6 +34,9 @@ type SourceConfig struct {
ID string
URL string
RTSPURLEnv string
Host string
Port int
Channel string
Transport string
Timeout time.Duration
LowLatency bool
@@ -202,11 +205,31 @@ func resolveSource(raw rawSource) (SourceConfig, error) {
}
if raw.Host != nil {
streamURL, err := structuredSourceURL(raw)
if err != nil {
return SourceConfig{}, err
host := strings.TrimSpace(*raw.Host)
if host == "" {
return SourceConfig{}, fmt.Errorf("source.host must be non-empty")
}
resolved.URL = streamURL
if raw.Username == nil || strings.TrimSpace(*raw.Username) == "" {
return SourceConfig{}, fmt.Errorf("source.username must be non-empty")
}
if raw.Password == nil || *raw.Password == "" {
return SourceConfig{}, fmt.Errorf("source.password must be non-empty")
}
port := 554
if raw.Port != nil {
port = *raw.Port
}
if port < 1 || port > 65535 {
return SourceConfig{}, fmt.Errorf("source.port must be between 1 and 65535")
}
channel := "101"
if raw.Channel != nil && strings.TrimSpace(*raw.Channel) != "" {
channel = strings.TrimSpace(*raw.Channel)
}
resolved.Host = host
resolved.Port = port
resolved.Channel = channel
resolved.URL = BuildRTSPURL(host, port, strings.TrimSpace(*raw.Username), *raw.Password, channel)
return resolved, nil
}
@@ -223,34 +246,6 @@ func resolveSource(raw rawSource) (SourceConfig, error) {
return resolved, nil
}
// structuredSourceURL builds an RTSP URL from local host/credential fields.
// net/url percent-encodes the userinfo, so a password with @/:/ does not break
// parsing. These fields are only permitted in the untracked local config.
func structuredSourceURL(raw rawSource) (string, error) {
host := strings.TrimSpace(*raw.Host)
if host == "" {
return "", fmt.Errorf("source.host must be non-empty")
}
if raw.Username == nil || strings.TrimSpace(*raw.Username) == "" {
return "", fmt.Errorf("source.username must be non-empty")
}
if raw.Password == nil || *raw.Password == "" {
return "", fmt.Errorf("source.password must be non-empty")
}
port := 554
if raw.Port != nil {
port = *raw.Port
}
if port < 1 || port > 65535 {
return "", fmt.Errorf("source.port must be between 1 and 65535")
}
channel := "101"
if raw.Channel != nil && strings.TrimSpace(*raw.Channel) != "" {
channel = strings.TrimSpace(*raw.Channel)
}
return BuildRTSPURL(host, port, strings.TrimSpace(*raw.Username), *raw.Password, channel), nil
}
// BuildRTSPURL assembles a Hikvision RTSP URL with percent-encoded credentials.
func BuildRTSPURL(host string, port int, username, password, channel string) string {
address := url.URL{