Files
SecMPS/doc/设计文档/网关Owl模块整改方案_v1.0.md

15 KiB
Raw Blame History

网关 Owl 模块整改方案 v1.0

版本: 1.0 日期: 2026-06-03 基准: doc/设计文档/网关owl模块检查报告20260603.md 架构原则: 遵循网关设计原则 §3.2-3.4(显式、异步、统一分页、弹性 Extra、不修改已有接口签名


1. 整改总览

阶段 优先级 内容 涉及文件 预计
O1 P0 设备通道展开 + OwlDevice 模型补全 OwlAdapter.cs + OwlModels.cs 2h
O2 P0 AI 事件接入 IHasAlarms OwlAdapter.cs + OwlModels.cs 2h
O3 P1 回放取流修正 + PTZ 预设位 OwlAdapter.cs 1h
O4 P2 AI 检测启停(IAcceptsControl) OwlAdapter.cs 1h
O5 P2 推流/拉流管理(可选独立路由) Program.cs + OwlAdapter 1.5h
O6 验证 全量编译 + 联调 1h
合计 5 文件 ~8.5h

2. 阶段 O1: 设备通道展开 + 模型补全(预计 2h

2.1 现状与问题

// 当前: GET /devices → 只返回NVR父设备
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(...)
{
    var json = await client.GetStringAsync($"/devices?page={page}&size={size}");
    // MapDevice: IsParent=true, Category="硬盘录像机" — 无通道子设备
}

后果: Vol.Pro 设备列表只有 NVR前端"预览"按钮找不到摄像头通道。

2.2 整改设计

改用 GET /devices/channels — Owl 的联合接口直接返回设备+通道的扁平列表:

{
  "items": [
    { "id": "mp123", "type": "DEVICE", "name": "NVR-01", "is_online": "1", "channel_count": 4, ... },
    { "id": "mp123/34020000001320000001", "type": "CHANNEL", "did": "mp123", "name": "仓库入口", "is_online": true, "ptztype": 1, ... },
    { "id": "mp123/34020000001320000002", "type": "CHANNEL", "did": "mp123", "name": "仓库后门", "is_online": true, "ptztype": 0, ... }
  ],
  "total": 3
}

映射逻辑(单次请求完成父子映射):

// OwlDeviceChannel 联合模型
public class OwlDeviceChannel
{
    public string? Id { get; set; }
    public string? Type { get; set; }         // "DEVICE" | "CHANNEL"
    public string? Did { get; set; }          // 通道所属设备ID
    public string? Name { get; set; }
    public string? IsOnline { get; set; }     // DEVICE: "1"/"0", CHANNEL: true/false
    public string? Manufacturer { get; set; }
    public string? Model { get; set; }
    public string? Firmware { get; set; }
    public string? Longitude { get; set; }
    public string? Latitude { get; set; }
    public int? ChannelCount { get; set; }
    public int? Ptztype { get; set; }         // CHANNEL: 0=无云台, 1=方向, 2=预置位
    public string? App { get; set; }          // CHANNEL: 流应用名
    public string? StreamId { get; set; }     // CHANNEL: 流ID
    // ... 其他字段
}

GetDevicesAsync 重写

public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null)
{
    await _limiter.WaitAsync();
    var client = await _auth.GetAuthenticatedClientAsync();
    var url = $"/devices/channels?page={page}&size=1000";  // 大pageSize一次性获取
    if (!string.IsNullOrEmpty(keyword)) url += $"&key={Uri.EscapeDataString(keyword)}";
    var json = await client.GetStringAsync(url);
    var result = JsonSerializer.Deserialize<OwlPagedResult<OwlDeviceChannel>>(json)!;

    var devices = new List<StandardDevice>();

    // 第一遍: 映射 DEVICE 为父设备
    var deviceItems = result.Items.Where(x => x.Type == "DEVICE").ToList();
    var channelItems = result.Items.Where(x => x.Type == "CHANNEL").ToList();

    foreach (var d in deviceItems)
    {
        // 收集该设备的通道
        var childChannels = channelItems.Where(c => c.Did == d.Id).ToList();

        devices.Add(new StandardDevice
        {
            SourceId = d.Id ?? "",
            Name = d.Name ?? d.Id ?? "",
            Category = "硬盘录像机",
            Group = "视频设备",
            IsOnline = d.IsOnline == "1",
            IsParent = true,
            IpAddress = d.Address,
            Extra = new Dictionary<string, object?>
            {
                ["manufacturer"] = d.Manufacturer,
                ["model"] = d.Model,
                ["firmware"] = d.Firmware,
                ["longitude"] = d.Longitude,
                ["latitude"] = d.Latitude,
                ["channelCount"] = d.ChannelCount ?? childChannels.Count
            }
        });

        // 映射通道为子设备
        foreach (var ch in childChannels)
        {
            devices.Add(new StandardDevice
            {
                SourceId = ch.Id ?? "",
                Name = ch.Name ?? $"通道{ch.Id}",
                Category = "摄像机",
                Group = "视频设备",
                IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",
                IsParent = false,
                ParentSourceId = d.Id,
                Extra = new Dictionary<string, object?>
                {
                    ["hasPtz"] = (ch.Ptztype ?? 0) > 0 ? "1" : "0",
                    ["app"] = ch.App,
                    ["streamId"] = ch.StreamId
                }
            });
        }
    }

    return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
}

2.3 影响分析

影响点 说明
前端预览按钮 现在能找到 DeviceGroup=视频设备, IsParent=否 的通道子设备,预览按钮可用
设备树同步 A3 同步时有父子关系,ParentSourceId 解析为父设备 DeviceId
视频墙 摄像机通道列表包含 hasPtz 标识,云台面板按需显示
MC4/IoT 零影响 — 不同适配器独立运行

3. 阶段 O2: AI 事件接入 IHasAlarms预计 2h

3.1 现状态

OwlAdapter 没有实现 IHasAlarmsAI 事件走不到 Vol.Pro。

3.2 整改设计

OwlAdapter 增加 IHasAlarms 实现

public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush, IHasAlarms
{
    // Capabilities 增加 HasAlarms = true

    /// <summary>GET /events → StandardAlarm[]</summary>
    public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(
        int page, int size, DateTime from, DateTime to, string? level = null, string? state = null)
    {
        await _limiter.WaitAsync();
        var client = await _auth.GetAuthenticatedClientAsync();
        var fromMs = new DateTimeOffset(from).ToUnixTimeMilliseconds();
        var toMs = new DateTimeOffset(to).ToUnixTimeMilliseconds();
        var url = $"/events?page={page}&size={size}&start_ms={fromMs}&end_ms={toMs}";
        var json = await client.GetStringAsync(url);
        var result = JsonSerializer.Deserialize<OwlPagedResult<OwlAiEvent>>(json)!;

        return new PagedResult<StandardAlarm>
        {
            Items = result.Items.Select(MapEventToAlarm).ToList(),
            Total = result.Total
        };
    }

    private StandardAlarm MapEventToAlarm(OwlAiEvent e) => new()
    {
        AlarmId = $"owl-ai-{e.Id}",
        AdapterCode = AdapterCode,
        Level = e.Label switch {
            "person" => "重要",
            "car" => "重要", 
            _ => "普通"
        },
        Title = $"AI检测: {e.Label} (置信度 {e.Score:P0})",
        Content = $"通道{e.Cid}: {e.Zones ?? ""}",
        OccurTime = DateTimeOffset.FromUnixTimeMilliseconds(e.StartedAt ?? 0).DateTime,
        Status = e.EndedAt > 0 ? "已结束" : "未确认",
        Extra = new Dictionary<string, object?>
        {
            ["imagePath"] = e.ImagePath,
            ["score"] = e.Score,
            ["label"] = e.Label,
            ["model"] = e.Model
        }
    };

    public async Task ConfirmAlarmAsync(string alarmId) { /* AI事件不支持确认 */ }
    public async Task EndAlarmAsync(string alarmId) { /* AI事件不支持结束 */ }
}

3.3 事件快照图片

网关注册一条 B-路由直接代理图片访问:

// 在 OwlAdapter 中增加
public async Task<byte[]> GetEventImageAsync(string imagePath)
{
    var client = await _auth.GetAuthenticatedClientAsync();
    return await client.GetByteArrayAsync($"/events/image/{imagePath}");
}

// Program.cs 加路由
app.MapGet("/api/gateway/owl/image/{*path}", async (string path, AdapterRegistry registry) =>
{
    var owl = registry.FindByCode<OwlAdapter>("Owl:main");
    if (owl == null) return Results.NotFound();
    var bytes = await owl.GetEventImageAsync(path);
    return Results.File(bytes, "image/jpeg");
});

3.4 后端 DTO 补充

/// <summary>Owl AI 事件</summary>
public class OwlAiEvent
{
    public long? Id { get; set; }
    public string? Did { get; set; }          // 设备ID
    public string? Cid { get; set; }          // 通道ID
    public long? StartedAt { get; set; }      // 毫秒时间戳
    public long? EndedAt { get; set; }
    public string? Label { get; set; }        // person / car / ...
    public float? Score { get; set; }         // 0.0-1.0
    public string? Zones { get; set; }        // 检测区域JSON
    public string? ImagePath { get; set; }
    public string? Model { get; set; }
}

4. 阶段 O3: 回放修正 + PTZ 扩展(预计 1h

4.1 GetPlaybackUrlAsync 修正

当前手工拼 URL改为调用 Owl API

GoWVP 文档中播放接口 POST /channels/{id}/play 返回的 PlayOutput.Items[] 包含 Hls 字段。录像回放无需额外接口——同一个 HLS 地址加上 start_ms/end_ms 参数即可。

方案: 保持当前实现(手工拼 URL 是 Owl 的约定用法),增加 URL 不存在时的 fallback

public async Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end)
{
    await _limiter.WaitAsync();
    var client = await _auth.GetAuthenticatedClientAsync();
    var token = await _auth.GetTokenAsync();
    var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
    var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
    var baseAddr = client.BaseAddress?.ToString().TrimEnd('/') ?? "";
    return new StreamUrls
    {
        Hls = $"{baseAddr}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
    };
}

变化:client.BaseAddress → 实际 Owl 地址(之前隐式依赖 HttpClient.BaseAddress 已包含)。

4.2 PTZ 预设位/巡航

PtzControlAsync 增加 action 参数透传:

public async Task PtzPresetAsync(string channelId, int presetIndex)
{
    await _limiter.WaitAsync();
    var client = await _auth.GetAuthenticatedClientAsync();
    await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
        new { action = "preset", preset = presetIndex });
}

无需修改 IHasStreams 接口——PTZ 扩展通过 PtzControlAsync(direction: "preset_1") 或新增公开方法由 B-路由直接调用。


5. 阶段 O4: AI 检测启停(预计 1h

5.1 通过 IAcceptsControl 暴露

OwlAdapter 实现 IAcceptsControl(已在 KMS 适配器中新增的接口):

public class OwlAdapter : ..., IAcceptsControl
{
    public async Task<ControlResult> SendControlAsync(string sourceDeviceId, string command, Dictionary<string, object?> parameters)
    {
        await _limiter.WaitAsync();
        var client = await _auth.GetAuthenticatedClientAsync();
        try
        {
            switch (command)
            {
                case "ai-enable":
                    await client.PostAsync($"/channels/{sourceDeviceId}/ai/enable", null);
                    break;
                case "ai-disable":
                    await client.PostAsync($"/channels/{sourceDeviceId}/ai/disable", null);
                    break;
                case "zone-add":
                    await client.PostAsJsonAsync($"/channels/{sourceDeviceId}/zones", parameters!);
                    break;
                default:
                    return new ControlResult { Success = false, Message = $"不支持的指令: {command}" };
            }
            return new ControlResult { Success = true };
        }
        catch (Exception ex) { return new ControlResult { Success = false, Message = ex.Message }; }
    }
}

前端调用:POST /api/gateway/control/Owl:main { deviceId: "ch123", command: "ai-enable" }


6. 阶段 O5: 推流/拉流管理(可选项,预计 1.5h

6.1 设计决策

推流/拉流管理属于管理员操作而非实时数据查询。建议通过 B-组新路由暴露,不新增 Core 接口:

// Program.cs — 推流/拉流 CRUD 路由组
app.MapGet("/api/gateway/owl/stream-pushs", async (int page, int size, AdapterRegistry registry) => { ... });
app.MapPost("/api/gateway/owl/stream-pushs", async (StreamPushRequest req, ...) => { ... });
// ... 类推

6.2 推流请求模型

public class StreamPushRequest
{
    public string Name { get; set; } = "";
    public string App { get; set; } = "live";
    public string Stream { get; set; } = "";
    public bool? IsAuthEnabled { get; set; }
}

public class StreamProxyRequest
{
    public string Name { get; set; } = "";
    public string Type { get; set; } = "RTSP";
    public string App { get; set; } = "live";
    public string Stream { get; set; } = "";
    public string? SourceUrl { get; set; }
    public int? Transport { get; set; }
}

6.3 作用

管理端通过网关统一管理 Owl 视频源添加/删除/状态查询,无需单独登录 Owl 控制台。前端可加"添加摄像头"按钮调用这些路由。


7. 文件变更清单

文件 新增 修改 说明
OwlAdapter.cs GetDevicesAsync 重写 + IHasAlarms 实现 + IAcceptsControl 实现 + PTZ 预设位
OwlModels.cs (新建) OwlDeviceChannel + OwlAiEvent + DTO 完整化(从 OwlAdapter.cs 分离)
OwlAuthHelper.cs HealthCheck 端点改 /stats需确认
Program.cs AI 事件图片代理路由 + 推流/拉流路由O5
IAcceptsControl.cs 已存在KMS 阶段新增)

8. 与现有架构的兼容性

架构元素 影响
IHasFlatDevices 签名 不变 — GetDevicesAsync 签名不变,仅内部实现改为调 /devices/channels
IHasAlarms OwlAdapter 新增实现,零冲突 — KMS 也实现了 IHasAlarms
IAcceptsControl OwlAdapter 新增实现 — KMS 已有实现B10 路由自动发现
AdapterCapabilities 扩展 HasAlarms=true, FeatureFlags["aiEnable"]=true
Vol.Pro A3 同步 ParentSourceId 已有解析逻辑,新通道子设备自然被正确处理
前端 base_device.vue 无改动 — 操作列按钮按 DeviceGroup="视频设备" 自动匹配新展开的通道

9. 验证点

场景 预期
GET /api/gateway/devices?adapter=Owl:main 返回 NVR 父设备 + 通道子设备,子设备有 hasPtz Extra
Vol.Pro 设备列表 显示 Owl 摄像机通道AdapterCode=Owl:main
前端预览按钮 通道子设备显示"预览"按钮,点击播放实时流
GET /api/gateway/alarms/Owl:main 返回 AI 检测事件(人员/车辆等)
规则引擎 可将 Owl AI 事件作为告警源触发规则
POST /api/gateway/control/Owl:main ai-enable 远程开启 Owl AI 检测