# safesightd

`safesightd` 是 SafeSight Control 管理端后端服务，负责设备发现、设备注册表维护、代理访问设备端 `rk3588-agent`，并提供内嵌 Web UI、OpenAPI 页面和 HTTP API。

## 当前状态

- 提供 HTTP API，供前端或其他本地工具调用。
- 内嵌 Web UI，启动后可直接在浏览器访问。
- 支持 UDP 广播发现设备，并维护内存中的设备在线状态。
- 支持代理访问设备端 agent 的常用接口，以及 `/v1/*` 通用透传。
- 支持批量任务执行，并通过 SSE 推送逐设备状态。
- 模板从本地 `templates/*.json` 读取。

## 运行方式

默认通过单个可执行文件运行：

```bash
safesightd
```

也可以指定配置文件路径：

```bash
safesightd path/to/safesightd.json
```

在当前仓库的 Windows 开发环境中，推荐使用统一脚本入口：

```bat
scripts\safesightd.bat build
scripts\safesightd.bat start
scripts\safesightd.bat stop
scripts\safesightd.bat restart
scripts\safesightd.bat status
```

各动作说明：

- `build`: 编译 `.\cmd\safesightd`，生成根目录下的 `safesightd.exe`
- `start`: 启动当前仓库中的 `safesightd.exe`，并检查 `/health`
- `stop`: 停止当前仓库对应的 `safesightd.exe`
- `restart`: 先停止再启动
- `status`: 查看进程状态与健康检查结果

脚本实际入口文件：

- `scripts/safesightd.bat`
- `scripts/safesightd.ps1`

程序启动后：

- `GET /` 会重定向到 `/ui`
- `GET /ui` 为内嵌管理页面
- `GET /openapi.json` 返回 OpenAPI 描述
- `GET /health` 返回健康检查结果

当前仓库中的示例配置监听地址为 `0.0.0.0:18080`。

## 依赖

- Go `1.23.3`
- `github.com/go-chi/chi/v5`
- `github.com/go-chi/cors`
- `github.com/google/uuid`

## 主要模块

### Discovery

- 向所有可用广播网卡发送 UDP discover 请求
- 在本地临时 UDP 端口监听回复
- 按 `device_id` 去重，并使用最新回复更新注册表

### Device Registry

- 以内存维护设备列表
- 每 2 秒执行一次离线判定
- 超过 `offline_after_ms` 未更新则标记为离线
- 每 30 秒对在线设备拉取一次 `GET /v1/graphs`，更新 `graphs` 字段
- 除 UDP 发现外，成功代理调用设备接口时也会刷新设备 `last_seen_ms`

### Agent Client

- 默认 HTTP 请求超时为 3 秒
- 对配置下发、模型上传、`/v1/config/ui/*`、人脸库上传等长操作使用 120 秒超时
- 自动附带全局 `X-RK-Token`

### Template Service

- 从本地 `templates/` 目录加载 `.json` 模板
- 返回模板列表和单个模板详情
- 当前没有实现 embed 模板加载

### Task Runner

- 支持并发执行，默认并发数为 `concurrency`
- 支持 SSE 推送逐设备状态
- 当前支持的任务类型：
  - `config_apply`
  - `reload`
  - `rollback`
  - `media_start`
  - `media_restart`
  - `media_stop`

## API 概览

### 基础接口

- `GET /health`
- `GET /openapi.json`
- `GET /` -> 302 跳转到 `/ui`
- `GET /ui`

### Discovery

#### `POST /api/discovery/search`

Request:

```json
{ "timeout_ms": 1200 }
```

Response:

```json
{
  "items": [
    {
      "device_id": "...",
      "hostname": "...",
      "ip": "...",
      "agent_port": 9100,
      "media_port": 9000,
      "device_name": "...",
      "version": "...",
      "git_sha": "...",
      "uptime_sec": 0,
      "last_seen_ms": 0,
      "online": true
    }
  ]
}
```

### Devices

#### `GET /api/devices`

返回当前注册表中的设备列表：

```json
{
  "items": [
    {
      "device_id": "...",
      "hostname": "...",
      "ip": "...",
      "agent_port": 9100,
      "media_port": 9000,
      "device_name": "...",
      "version": "...",
      "git_sha": "...",
      "uptime_sec": 0,
      "last_seen_ms": 0,
      "online": true,
      "graphs": {}
    }
  ]
}
```

#### `POST /api/devices`

手工添加一个设备到注册表：

```json
{
  "device_id": "demo-device",
  "device_name": "Demo",
  "ip": "192.168.1.10",
  "agent_port": 9100,
  "media_port": 9000
}
```

#### `GET /api/devices/{id}`

直接返回该设备对象。

### 设备代理接口

以下接口由 safesightd 转发到设备端 agent：

- `GET /api/devices/{id}/info` -> `GET /v1/info`
- `POST /api/devices/{id}/reload` -> `POST /v1/media-server/reload`
- `POST /api/devices/{id}/rollback` -> `POST /v1/media-server/rollback`
- `GET /api/devices/{id}/graphs` -> `GET /v1/graphs`
- `GET /api/devices/{id}/graphs/{name}` -> `GET /v1/graphs/{name}`
- `GET /api/devices/{id}/logs?limit=200` -> `GET /v1/logs/recent?limit=200`
- `POST /api/devices/{id}/config/apply` -> `PUT /v1/config`
- `GET /api/devices/{id}/models` -> `GET /v1/models`
- `POST /api/devices/{id}/media-server/start` -> `POST /v1/media-server/start`
- `POST /api/devices/{id}/media-server/restart` -> `POST /v1/media-server/restart`
- `POST /api/devices/{id}/media-server/stop` -> `POST /v1/media-server/stop`
- `GET /api/devices/{id}/media-server/status` -> `GET /v1/media-server/status`

#### 通用透传

- `ANY /api/devices/{id}/v1/*`

该路由会将 `/api/devices/{id}` 之后的路径原样透传到设备端 agent，可用于：

- `/v1/config`
- `/v1/config/ui/schema`
- `/v1/config/ui/state`
- `/v1/config/ui/plan`
- `/v1/config/ui/apply`
- `/v1/face-gallery`
- `/v1/face-gallery/reload`
- 以及其他已存在的 `/v1/*` 接口

### 模型上传

#### `POST /api/devices/{id}/models/upload`

请求类型：`multipart/form-data`

字段：

- `name`: 模型名
- `file`: 二进制文件

行为：safesightd 读取上传文件，并转发为设备端 agent 的 `PUT /v1/models/{name}`。

### Templates

- `GET /api/templates`
- `GET /api/templates/{name}`

当前模板数据来自本地 `templates/*.json`。

### Tasks

#### `POST /api/tasks`

Request:

```json
{
  "type": "config_apply",
  "device_ids": ["device-a", "device-b"],
  "payload": {
    "config": {}
  }
}
```

说明：

- `config_apply` 会将 payload 作为配置内容下发到 `/v1/config`
- `media_start` 和 `media_restart` 可选接收 `{"config":"xxx"}` 形式的 payload

Response:

```json
{ "task_id": "..." }
```

#### `GET /api/tasks`

返回当前内存中的任务列表。

#### `GET /api/tasks/{id}/events`

SSE 事件名为 `device_update`，数据格式：

```json
{
  "device_id": "...",
  "status": "running",
  "progress": 0.0,
  "error": ""
}
```

## 配置文件

`safesightd.json` 示例：

```json
{
  "listen": "0.0.0.0:18080",
  "discovery_port": 35688,
  "discovery_timeout_ms": 1200,
  "offline_after_ms": 10000,
  "agent_token": "CHANGE_ME",
  "concurrency": 5
}
```

字段说明：

- `listen`: safesightd 监听地址
- `discovery_port`: 设备发现广播端口
- `discovery_timeout_ms`: 搜索超时
- `offline_after_ms`: 多久未更新则视为离线
- `agent_token`: 转发到设备端 agent 的统一 token
- `concurrency`: 批量任务并发数

## 目录说明

- `cmd/safesightd`: 程序入口
- `internal/api`: API 路由与处理器
- `internal/service`: discovery、registry、task、template、agent client
- `internal/web`: 内嵌 Web UI
- `templates`: 本地模板
- `scripts/deploy`: 部署脚本

## 验证现状

当前仓库已通过：

```bash
go test ./...
```
