Skill: jftech/jftech-open-pro-video-record (version 1.0.0)

--- name: jf-open-pro-video-record description: 杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址，包括录像列表查询、回放地址获取、录像下载等功能。使用场景：云存回放、本地卡录像回放、录像下载、历史视频查看。 # 必需凭证声明 - 平台元数据 credentials: required: - name: JF_UUID type: string description: 杰峰开放平台用户唯一标识 source: https://open.jftech.com/ - name: JF_APPKEY type: string description: 杰峰开放平台应用 Key source: https://open.jftech.com/ - name: JF_APPSECRET type: string description: 杰峰开放平台应用密钥 source: https://open.jftech.com/ - name: JF_MOVECARD type: integer description: 签名算法偏移量 (0-9) source: https://open.jftech.com/ optional: - name: JF_SN type: string description: 设备序列号 - name: JF_USERNAME type: string description: 设备登录用户名 default: admin - name: JF_PASSWORD type: string description: 设备登录密码 - name: JF_ENDPOINT type: string description: API 端点 default: api.jftechws.com # 网络端点声明 endpoints: - url: https://api.jftechws.com description: 杰峰官方 API (国际) - url: https://api-cn.jftech.com description: 杰峰官方 API (中国大陆) # 安全声明 security: credentials_required: true env_vars_only: true language: python network_access: - api.jftechws.com - api-cn.jftech.com file_access: none --- # JF Open Pro Video Record - 杰峰设备录像回放技能 > **面向开发者的杰峰设备录像回放工具 (Python)** > > 支持设备云存储录像和设备本地录像回放地址获取，包括录像列表查询、回放地址获取、录像下载等功能。 --- ## 🔒 安全说明 **凭据存储：仅支持环境变量** | 方式 | 支持 | 说明 | |------|------|------| | **环境变量** | ✅ 支持 | 推荐方式，避免凭据出现在进程列表或日志中 | | **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 | | **配置文件** | ❌ 不支持 | 避免明文存储凭据 | **网络访问：** - ✅ 仅访问杰峰官方 API 端点 (`api.jftechws.com` / `api-cn.jftech.com`) - ❌ 不访问第三方服务 - ❌ 不读取敏感系统文件 --- ## 🚀 快速开始 ### 设置环境变量 ```bash # 开放平台凭证（必需） export JF_UUID="your-uuid" # 开放平台用户唯一标识 export JF_APPKEY="your-appkey" # 开放平台应用 Key export JF_APPSECRET="your-appsecret" # 开放平台应用密钥 export JF_MOVECARD=5 # 签名算法偏移量 (0-9) # 设备信息 export JF_SN="your-device-sn" # 设备序列号 export JF_USERNAME="admin" # 设备登录用户名（可选，默认：admin） export JF_PASSWORD="your-password" # 设备登录密码（本地录像必需） export JF_ENDPOINT="api.jftechws.com" # API 端点（可选） ``` ### 使用技能 ```bash # ========== 云存录像 ========== # 获取云存视频列表（按时间范围查询） python scripts/cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" # 获取云存视频列表（带报警类型过滤） python scripts/cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect" # 获取云存回放地址（按时间范围） python scripts/cloud_playback_url.py --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" # 获取云存回放地址（按视频 ID 精准查询） python scripts/cloud_playback_url.py --video-id "xxxxxxxxxx" # 获取云存回放地址（下载 MP4 格式） python scripts/cloud_playback_url.py --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" --format MP4 # ========== 本地录像 ========== # 获取本地录像回放列表 python scripts/local_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" # 获取本地录像回放地址 python scripts/local_playback_url.py --file-name "/idea0/2026-04-07/001/15.23.26-15.23.36[R][@39733][2].h264" --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" # 本地录像下载（MP4 格式） python scripts/local_playback_url.py --file-name "xxx.h264" --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" --download ``` --- ## 📋 环境变量 | 变量名 | 说明 | 必需 | 默认值 | |--------|------|------|--------| | `JF_UUID` | 开放平台用户唯一标识 | 是 | - | | `JF_APPKEY` | 开放平台应用 Key | 是 | - | | `JF_APPSECRET` | 开放平台应用密钥 | 是 | - | | `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - | | `JF_SN` | 设备序列号 | 云存必需 | - | | `JF_USERNAME` | 设备登录用户名 | 本地录像必需 | `admin` | | `JF_PASSWORD` | 设备登录密码 | 本地录像必需 | - | | `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` | --- ## 🛠️ 功能 ### 1. 云存录像 #### 1.1 获取云存视频列表 查询设备在指定时间段内的云存视频列表。 **支持场景：** - 条件查询（时间范围） - 组合条件查询（分页 + 报警类型过滤） **支持的报警类型：** - `HumanDetect` - 人形检测 - `MotionDetect` - 移动侦测 - `appEventHumanDetectAlarm` - 人形报警 - 更多类型参考 [报警消息类型](https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=ba50abdc08e84216bf8e3d3742df8922&lang=zh) **使用示例：** ```bash # 按时间范围查询 python scripts/cloud_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" # 带报警类型过滤 python scripts/cloud_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" \ --events "HumanDetect" # 分页查询 python scripts/cloud_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" \ --page-start 1 \ --page-size 50 ``` **返回字段：** | 字段 | 说明 | 示例 | |------|------|------| | `StartTime` | 录像开始时间 | `2026-04-07 13:12:34` | | `StopTime` | 录像结束时间 | `2026-04-07 13:12:51` | | `IndexFile` | 录像文件名 | `xxx.m3u8` | | `PicFlag` | 是否有缩略图 (1=有，0=无) | `1` | | `VideoSize` | 视频大小（字节） | `339014` | | `thumbURL` | 缩略图 URL | `http://...` | | `events` | 报警类型列表 | `["HumanDetect"]` | | `videoId` | 视频 ID（精准查询用） | `0...9a...z` | #### 1.2 获取云存回放地址 获取云存视频回放或下载地址，支持 HLS 在线播放和 MP4 下载。 **支持模式：** - 精准查询：根据视频 ID 查询 - 条件查询：根据时间范围查询 **使用示例：** ```bash # 按时间范围获取回放地址（HLS 在线播放） python scripts/cloud_playback_url.py \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" # 按视频 ID 精准查询 python scripts/cloud_playback_url.py \ --video-id "xxxxxxxxxx" # 获取下载链接（MP4 格式） python scripts/cloud_playback_url.py \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --format MP4 # 多目设备（多镜头摄像头） python scripts/cloud_playback_url.py \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --multi-video ``` **播放方式：** ```bash # VLC 播放 vlc "https://xxx.com/xxx.m3u8?Expires=..." # 网页播放（HLS.js） <video src="https://xxx.com/xxx.m3u8?Expires=..." controls></video> # 下载 MP4 curl -o video.mp4 "https://xxx.com/xxx.mp4?Expires=..." # 或 ffmpeg -i "https://xxx.com/xxx.mp4?Expires=..." -c copy video.mp4 ``` **注意事项：** - 回放地址有效期：**24 小时** - MP4 下载按**文件大小流量计费** - 多目设备返回多个地址，以分号 `;` 分隔 --- ### 2. 本地录像（TF 卡/硬盘） #### 2.1 获取本地录像回放列表 查询设备本地存储（TF 卡或硬盘）中的录像文件列表。 **前置条件：** - 设备支持卡存录像（有 TF 卡或硬盘） - 需配置设备登录凭据（`JF_USERNAME`、`JF_PASSWORD`） **录像类型说明：** | 类型 | 说明 | |------|------| | `*` | 所有类型的录像 | | `R` | 常规录像（无报警时的连续录像，含 AOV 录像） | | `A` | 非视频类报警（如 IO 口报警） | | `M` | 视频类报警（移动侦测、人形检测等） | | `H` | 手动录像 | | `C` | 卡号录像 | | `V` | AOV 录像（低功耗全时录像） | | `I` | 入侵报警 | | `S` | 盗移/滞留报警 | | `F` | 人脸识别录像 | | `N` | 车牌识别录像 | | `K` | 关键录像 | **使用示例：** ```bash # 查询所有类型录像 python scripts/local_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" # 只查询报警录像（移动侦测 + 人形检测 + 常规） python scripts/local_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" \ --event "AMRH" # 只查询常规录像 python scripts/local_video_list.py \ --start-time "2026-04-07 10:00:00" \ --stop-time "2026-04-07 18:00:00" \ --event "R" ``` **返回字段：** | 字段 | 说明 | 示例 | |------|------|------| | `BeginTime` | 录像开始时间 | `2026-04-07 20:00:00` | | `EndTime` | 录像结束时间 | `2026-04-07 21:00:00` | | `FileName` | 录像文件路径 | `/idea0/2026-04-07/002/20.00.00-21.00.00[R][@2dcc5][0].h264` | | `FileLength` | 文件大小（KB） | `123456` | #### 2.2 获取本地录像回放地址 获取本地录像文件的回放或下载地址。 **支持的协议格式：** | 协议 | 格式 | 说明 | |------|------|------| | `flv` | FLV | 标准 FLV 封装（H.265 采用国内行业 FLV 标准） | | `flv-enhanced` | FLV-Enhanced | H.265 标准 FLV-Enhanced 封装 | | `hls-ts` | HLS-TS | HLS 协议，TS 格式切片 | | `hls-fmp4` | HLS-fMP4 | HLS 协议，fMP4 格式切片 | | `mp4` | MP4 | HTTP 协议，MP4 格式（用于下载） | | `rtsp-sdp` | RTSP-SDP | RTSP 标准协议（默认） | | `rtsp-pri` | RTSP-PRI | RTSP 私有协议 | **使用示例：** ```bash # 在线播放（FLV 格式） python scripts/local_playback_url.py \ --file-name "/idea0/2026-04-07/001/15.23.26-15.23.36[R][@39733][2].h264" \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --protocol flv # HLS 播放 python scripts/local_playback_url.py \ --file-name "xxx.h264" \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --protocol hls-ts # 录像下载（MP4 格式） python scripts/local_playback_url.py \ --file-name "xxx.h264" \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --protocol mp4 \ --download # 指定码流类型（0=主码流/高清，1=辅码流/标清） python scripts/local_playback_url.py \ --file-name "xxx.h264" \ --start-time "2026-04-07 15:23:26" \ --stop-time "2026-04-07 15:23:36" \ --stream-type 0 ``` **注意事项：** - 回放地址有效期：**10 小时** - 同时只支持**一路回放或下载** - 本地录像回放和下载**按流量计费** - 必须先获取录像文件列表，使用返回的 `FileName` 字段 --- ## ⚠️ 错误处理 | 错误码 | 说明 | 解决方案 | |--------|------|----------| | `2000` | 成功 | - | | `12504` | 授权失败 - 设备未开通服务 | 登录开放平台为设备绑定对应套餐 | | `10001` | 参数错误 | 检查请求参数格式 | | `10002` | 签名失败 | 检查 appKey/appSecret 和时间戳 | | `200` | 设备响应成功 | - | | `401` | 设备认证失败 | 检查设备用户名/密码 | ### 常见错误 **1. 云存服务未开通** ``` code: 12504 msg: authorize failed, Please check it in the open platform ``` → 登录杰峰开放平台，为设备购买并绑定云存套餐卡 **2. 本地录像文件不存在** ``` Ret: 404 msg: File not found ``` → 检查文件名是否正确，确认设备 TF 卡/硬盘中有录像 **3. 设备登录失败** ``` Ret: 401 msg: Authentication failed ``` → 检查 `JF_USERNAME` 和 `JF_PASSWORD` 是否正确 --- ## 📚 官方参考资料 - **杰峰开放平台**: https://open.jftech.com/ - **API 文档**: https://docs.jftech.com/ - **云存视频列表**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=66142b2ca13c418d84085772a627d650 - **云存回放地址**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672 - **本地录像列表**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=4b1516da5763439a9bc7175d7ac7d246 - **本地录像回放**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=4b1516da5763439a9bc7175d7ac7d246 - **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆) --- ## 📁 脚本工具 **云存录像脚本：** | 脚本 | 功能 | |------|------| | `cloud_video_list.py` | 获取云存视频列表 | | `cloud_playback_url.py` | 获取云存回放地址 | **本地录像脚本：** | 脚本 | 功能 | |------|------| | `local_video_list.py` | 获取本地录像回放列表 | | `local_playback_url.py` | 获取本地录像回放地址 | ```bash # 获取帮助 python scripts/cloud_video_list.py --help python scripts/cloud_playback_url.py --help python scripts/local_video_list.py --help python scripts/local_playback_url.py --help ``` --- **技能版本：** v1.0.1 **语言：** Python **最后更新：** 2026-04-08

path: skill.yaml
content: # JF Open Pro Video Record - Skill Registry Metadata
# This file defines the skill's requirements for ClawHub registry

name: jf-open-pro-video-record
version: 1.0.1
description: 杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址，包括录像列表查询、回放地址获取、录像下载等功能。

# Runtime requirements
runtime:
  language: python
  minVersion: "3.8"

# Required environment variables (credentials)
requiredEnvVars:
  - name: JF_UUID
    description: 杰峰开放平台用户唯一标识
    source: https://open.jftech.com/
  - name: JF_APPKEY
    description: 杰峰开放平台应用 Key
    source: https://open.jftech.com/
  - name: JF_APPSECRET
    description: 杰峰开放平台应用密钥
    source: https://open.jftech.com/
  - name: JF_MOVECARD
    description: 签名算法偏移量 (0-9)，用于时间戳偏移增加签名安全性
    source: https://open.jftech.com/

# Optional environment variables
optionalEnvVars:
  - name: JF_SN
    description: 设备序列号
  - name: JF_USERNAME
    description: 设备登录用户名
    default: admin
  - name: JF_PASSWORD
    description: 设备登录密码（本地录像必需）
  - name: JF_ENDPOINT
    description: API 端点
    default: api.jftechws.com

# Network endpoints (for firewall/security configuration)
endpoints:
  - url: https://api.jftechws.com
    description: 杰峰官方 API (国际)
  - url: https://api-cn.jftech.com
    description: 杰峰官方 API (中国大陆)

# Security declarations
security:
  credentialsRequired: true
  envVarsOnly: true
  networkAccess:
    - api.jftechws.com
    - api-cn.jftech.com
  fileAccess: none

# Entry points
scripts:
  - name: cloud_video_list.py
    description: 获取云存视频列表
    entryPoint: scripts/cloud_video_list.py
  - name: cloud_playback_url.py
    description: 获取云存回放地址
    entryPoint: scripts/cloud_playback_url.py
  - name: local_video_list.py
    description: 获取本地录像回放列表
    entryPoint: scripts/local_video_list.py
  - name: local_playback_url.py
    description: 获取本地录像回放地址
    entryPoint: scripts/local_playback_url.py

# Tags for discovery
tags:
  - jf-tech
  - 杰峰
  - video-record
  - playback
  - cloud-storage
  - local-storage
  - 云存回放
  - 本地录像


path: _meta.json
content: {
  "name": "jf-open-pro-video-record",
  "version": "1.0.1",
  "description": "杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址，包括录像列表查询、回放地址获取、录像下载等功能。",
  "requiredEnvVars": [
    {
      "name": "JF_UUID",
      "description": "杰峰开放平台用户唯一标识",
      "source": "https://open.jftech.com/"
    },
    {
      "name": "JF_APPKEY",
      "description": "杰峰开放平台应用 Key",
      "source": "https://open.jftech.com/"
    },
    {
      "name": "JF_APPSECRET",
      "description": "杰峰开放平台应用密钥",
      "source": "https://open.jftech.com/"
    },
    {
      "name": "JF_MOVECARD",
      "description": "签名算法偏移量 (0-9)，用于时间戳偏移增加签名安全性",
      "source": "https://open.jftech.com/"
    }
  ],
  "optionalEnvVars": [
    {
      "name": "JF_SN",
      "description": "设备序列号"
    },
    {
      "name": "JF_USERNAME",
      "description": "设备登录用户名",
      "default": "admin"
    },
    {
      "name": "JF_PASSWORD",
      "description": "设备登录密码（本地录像必需）"
    },
    {
      "name": "JF_ENDPOINT",
      "description": "API 端点",
      "default": "api.jftechws.com"
    }
  ],
  "credentialsRequired": true,
  "envVarsOnly": true,
  "networkAccess": [
    "api.jftechws.com",
    "api-cn.jftech.com"
  ],
  "fileAccess": "none",
  "language": "python",
  "scripts": [
    "cloud_video_list.py",
    "cloud_playback_url.py",
    "local_video_list.py",
    "local_playback_url.py"
  ]
}


path: scripts/cloud_playback_url.py
content: #!/usr/bin/env python3
"""
获取云存视频回放或下载地址脚本
"""

import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError


def generate_timestamp():
    """生成 20 位时间戳（毫秒）"""
    return str(int(time.time() * 1000)).zfill(20)


def str2byte(s):
    """字符串转字节数组"""
    return list(s.encode('utf-8'))


def change(encrypt_str, move_card):
    """移位算法"""
    encrypt_byte = str2byte(encrypt_str)
    length = len(encrypt_byte)
    for idx in range(length):
        tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
        encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
    return encrypt_byte


def merge_byte(encrypt_byte, change_byte):
    """合并字节数组"""
    length = len(encrypt_byte)
    temp = [0] * (length * 2)
    for idx in range(length):
        temp[idx] = encrypt_byte[idx]
        temp[length * 2 - 1 - idx] = change_byte[idx]
    return temp


def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
    """生成杰峰 API 签名"""
    encrypt_str = uuid + app_key + app_secret + time_millis
    encrypt_byte = str2byte(encrypt_str)
    change_byte = change(encrypt_str, movecard)
    merged_byte = merge_byte(encrypt_byte, change_byte)
    return hashlib.md5(bytes(merged_byte)).hexdigest()


def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
    """通过设备序列号生成 deviceToken"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/token"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    body = {"sns": [sn], "accessToken": ""}
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except URLError as e:
        return {"error": f"URL Error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}


def get_cloud_playback_url(device_token, uuid, app_key, app_secret, movecard=5,
                           start_time=None, stop_time=None, video_id=None,
                           channel=0, file_format="m3u8", multi_video=False,
                           endpoint="api.jftechws.com"):
    """获取云存视频回放或下载地址"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    
    body = {"channel": channel, "fileFormat": file_format}
    if video_id:
        body["videoId"] = video_id
    elif start_time and stop_time:
        body["startTime"] = start_time
        body["stopTime"] = stop_time
    else:
        raise ValueError("必须提供 videoId 或 start_time+stop_time")
    
    if multi_video:
        body["multiVideo"] = "1"
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except URLError as e:
        return {"error": f"URL Error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}


def get_config_from_env():
    """从环境变量读取配置"""
    required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
    missing_vars = [var for var in required_vars if not os.environ.get(var)]
    if missing_vars:
        raise Exception(f"缺少必需的环境变量：{', '.join(missing_vars)}")
    
    return {
        'uuid': os.environ.get('JF_UUID'),
        'appkey': os.environ.get('JF_APPKEY'),
        'appsecret': os.environ.get('JF_APPSECRET'),
        'movecard': int(os.environ.get('JF_MOVECARD', 5)),
        'sn': os.environ.get('JF_SN'),
        'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
    }


def main():
    parser = argparse.ArgumentParser(description='获取云存视频回放或下载地址')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--start-time', help='录像开始时间（YYYY-mm-dd HH:MM:SS）')
    group.add_argument('--video-id', help='视频 ID（精准查询）')
    parser.add_argument('--stop-time', help='录像结束时间')
    parser.add_argument('--channel', type=int, default=0, help='设备通道号')
    parser.add_argument('--format', choices=['m3u8', 'MP4'], default='m3u8', help='视频格式')
    parser.add_argument('--multi-video', action='store_true', help='多目设备')
    
    args = parser.parse_args()
    
    try:
        config = get_config_from_env()
    except Exception as e:
        print(f'❌ 配置错误：{e}')
        sys.exit(1)
    
    print("=" * 70)
    print("🎬 获取云存视频回放地址")
    print("=" * 70)
    print(f"设备 SN: {config['sn']}")
    if args.video_id:
        print(f"视频 ID: {args.video_id}")
    else:
        print(f"时间范围：{args.start_time} - {args.stop_time}")
    print(f"格式：{args.format}")
    print()
    
    print(">>> 获取设备 Token...")
    token_result = get_device_token(config['sn'], config['uuid'], config['appkey'], 
                                    config['appsecret'], config['movecard'], config['endpoint'])
    
    if token_result.get('error') or token_result.get('code') != 2000:
        print(f"❌ 获取设备 Token 失败：{token_result.get('error') or token_result.get('msg')}")
        sys.exit(1)
    
    device_token = token_result['data'][0]['token']
    print(f"✅ 设备 Token 获取成功")
    print()
    
    print(">>> 获取回放地址...")
    result = get_cloud_playback_url(
        device_token=device_token, uuid=config['uuid'], app_key=config['appkey'],
        app_secret=config['appsecret'], movecard=config['movecard'],
        start_time=args.start_time, stop_time=args.stop_time, video_id=args.video_id,
        channel=args.channel, file_format=args.format, multi_video=args.multi_video,
        endpoint=config['endpoint']
    )
    
    if result.get('error'):
        print(f"❌ 获取回放地址失败：{result['error']}")
        sys.exit(1)
    
    if result.get('code') != 2000:
        print(f"❌ API 错误码：{result.get('code')} - {result.get('msg')}")
        sys.exit(1)
    
    play_url = result.get('data', {}).get('url')
    if not play_url:
        print("❌ 未找到播放 URL")
        sys.exit(1)
    
    print("✅ 回放地址获取成功！")
    print()
    print("=" * 70)
    print("🔗 播放地址")
    print("=" * 70)
    print(f"{play_url}")
    print()
    print("⚠️ 回放地址有效期 24 小时")
    print()
    # 仅输出必要信息，避免泄露完整响应数据
    print("=" * 70)
    print("📋 响应摘要")
    print("=" * 70)
    print()
    print(json.dumps({"code": result.get('code'), "msg": result.get('msg')}, indent=2, ensure_ascii=False))
    
    sys.exit(0)


if __name__ == '__main__':
    main()


path: scripts/cloud_video_list.py
content: #!/usr/bin/env python3
"""
获取云存视频列表脚本

支持场景：
1. 条件查询（时间范围）- 根据起始时间和结束时间查询视频列表
2. 组合条件查询（分页 + 报警类型）- 精准查询特定报警类型的视频

官方文档：
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=66142b2ca13c418d84085772a627d650

API 端点：
POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoList/{deviceToken}

用法:
    export JF_UUID="your-uuid"
    export JF_APPKEY="your-appkey"
    export JF_APPSECRET="your-appsecret"
    export JF_MOVECARD=5
    export JF_SN="your-device-sn"
    
    # 按时间范围查询
    python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
    
    # 带报警类型过滤
    python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect"
"""

import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError


def generate_timestamp():
    """生成 20 位时间戳（毫秒）"""
    return str(int(time.time() * 1000)).zfill(20)


def str2byte(s):
    """字符串转字节数组（UTF-8 编码）"""
    return list(s.encode('utf-8'))


def change(encrypt_str, move_card):
    """移位算法"""
    encrypt_byte = str2byte(encrypt_str)
    length = len(encrypt_byte)
    for idx in range(length):
        tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
        encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
    return encrypt_byte


def merge_byte(encrypt_byte, change_byte):
    """合并字节数组"""
    length = len(encrypt_byte)
    temp = [0] * (length * 2)
    for idx in range(length):
        temp[idx] = encrypt_byte[idx]
        temp[length * 2 - 1 - idx] = change_byte[idx]
    return temp


def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
    """生成杰峰 API 签名（复杂加密算法）"""
    encrypt_str = uuid + app_key + app_secret + time_millis
    encrypt_byte = str2byte(encrypt_str)
    change_byte = change(encrypt_str, movecard)
    merged_byte = merge_byte(encrypt_byte, change_byte)
    return hashlib.md5(bytes(merged_byte)).hexdigest()


def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
    """通过设备序列号生成 deviceToken"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/token"
    
    headers = {
        "uuid": uuid,
        "appKey": app_key,
        "timeMillis": time_millis,
        "signature": signature,
        "Content-Type": "application/json"
    }
    
    # 使用 sns 参数（杰峰 API 要求）
    body = {"sns": [sn], "accessToken": ""}
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    
    try:
        with urlopen(req, timeout=30) as response:
            result = json.loads(response.read().decode('utf-8'))
            return result
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except URLError as e:
        return {"error": f"URL Error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}


def get_cloud_video_list(device_token, sn, start_time, stop_time, uuid, app_key, 
                         app_secret, movecard=5, channel=0, page_start=1, 
                         page_size=200, events=None, endpoint="api.jftechws.com"):
    """获取云存视频列表"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoList/{device_token}"
    
    headers = {
        "uuid": uuid,
        "appKey": app_key,
        "timeMillis": time_millis,
        "signature": signature,
        "Content-Type": "application/json"
    }
    
    body = {
        "startTime": start_time,
        "stopTime": stop_time,
        "sn": sn,
        "channel": channel,
        "pageStart": page_start,
        "pageSize": page_size
    }
    
    if events:
        body["events"] = events if isinstance(events, list) else [events]
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    
    try:
        with urlopen(req, timeout=30) as response:
            result = json.loads(response.read().decode('utf-8'))
            return result
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except URLError as e:
        return {"error": f"URL Error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}


def get_config_from_env():
    """从环境变量读取配置"""
    required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
    missing_vars = [var for var in required_vars if not os.environ.get(var)]
    
    if missing_vars:
        raise Exception(f"缺少必需的环境变量：{', '.join(missing_vars)}\n"
                       f"请设置：export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
    
    return {
        'uuid': os.environ.get('JF_UUID'),
        'appkey': os.environ.get('JF_APPKEY'),
        'appsecret': os.environ.get('JF_APPSECRET'),
        'movecard': int(os.environ.get('JF_MOVECARD', 5)),
        'sn': os.environ.get('JF_SN'),
        'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
    }


def format_video_list(result):
    """格式化视频列表输出"""
    if result.get('error'):
        return f"❌ 错误：{result['error']}"
    
    if result.get('code') != 2000:
        return f"❌ API 错误码：{result.get('code')}\n   详情：{result.get('msg', 'Unknown error')}"
    
    data = result.get('data', {})
    videos = data.get('VideoArray', [])
    
    if not videos:
        return "📭 未找到匹配的视频"
    
    output = []
    output.append(f"✅ 找到 {len(videos)} 个视频片段")
    output.append(f"   总记录数：{data.get('total', 'N/A')}")
    output.append(f"   页码：{data.get('pageNum', 'N/A')}")
    output.append(f"   是否页尾：{data.get('isFinished', 'N/A')}")
    output.append("")
    
    for i, video in enumerate(videos, 1):
        output.append(f"📹 视频 {i}:")
        output.append(f"   时间：{video.get('StartTime', 'N/A')} - {video.get('StopTime', 'N/A')}")
        output.append(f"   文件名：{video.get('IndexFile', 'N/A')}")
        output.append(f"   大小：{video.get('VideoSize', 0) / 1024:.1f} KB")
        output.append(f"   缩略图：{'有' if video.get('PicFlag') == 1 else '无'}")
        if video.get('events'):
            output.append(f"   报警类型：{', '.join(video.get('events', []))}")
        if video.get('videoId'):
            output.append(f"   视频 ID: {video.get('videoId')}")
        output.append("")
    
    return "\n".join(output)


def main():
    parser = argparse.ArgumentParser(
        description='获取云存视频列表',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
环境变量:
  JF_UUID       开放平台用户唯一标识 (必需)
  JF_APPKEY     开放平台应用 Key (必需)
  JF_APPSECRET  开放平台应用密钥 (必需)
  JF_MOVECARD   签名算法偏移量，通常设为 5 (必需)
  JF_SN         设备序列号 (必需)
  JF_ENDPOINT   API 端点，默认 api.jftechws.com (可选)

报警类型示例:
  HumanDetect           - 人形检测
  MotionDetect          - 移动侦测
  appEventHumanDetectAlarm - 人形报警

示例:
  # 按时间范围查询
  python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
  
  # 带报警类型过滤
  python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect"
  
  # 分页查询
  python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --page-start 1 --page-size 50
        '''
    )
    
    parser.add_argument('--start-time', required=True, help='录像查询开始时间（YYYY-mm-dd HH:MM:SS）')
    parser.add_argument('--stop-time', required=True, help='录像查询结束时间（YYYY-mm-dd HH:MM:SS）')
    parser.add_argument('--channel', type=int, default=0, help='设备通道号（默认 0）')
    parser.add_argument('--page-start', type=int, default=1, help='起始页（从 1 开始，默认 1）')
    parser.add_argument('--page-size', type=int, default=200, help='分页大小（1-200，默认 200）')
    parser.add_argument('--events', nargs='+', help='报警类型列表（可选，用于过滤）')
    parser.add_argument('--json', action='store_true', help='输出 JSON 格式')
    
    args = parser.parse_args()
    
    try:
        config = get_config_from_env()
    except Exception as e:
        print(f'❌ 配置错误：{e}')
        sys.exit(1)
    
    print("=" * 70)
    print("📋 获取云存视频列表")
    print("=" * 70)
    print()
    print(f"设备 SN: {config['sn']}")
    print(f"时间范围：{args.start_time} - {args.stop_time}")
    if args.events:
        print(f"报警类型：{', '.join(args.events)}")
    print()
    
    print(">>> 获取设备 Token...")
    token_result = get_device_token(
        config['sn'], config['uuid'], config['appkey'], 
        config['appsecret'], config['movecard'], config['endpoint']
    )
    
    if token_result.get('error') or token_result.get('code') != 2000:
        print(f"❌ 获取设备 Token 失败：{token_result.get('error') or token_result.get('msg')}")
        sys.exit(1)
    
    device_token = token_result['data'][0]['token']
    print(f"✅ 设备 Token 获取成功")
    print()
    
    print(">>> 获取云存视频列表...")
    result = get_cloud_video_list(
        device_token=device_token,
        sn=config['sn'],
        start_time=args.start_time,
        stop_time=args.stop_time,
        uuid=config['uuid'],
        app_key=config['appkey'],
        app_secret=config['appsecret'],
        movecard=config['movecard'],
        channel=args.channel,
        page_start=args.page_start,
        page_size=args.page_size,
        events=args.events,
        endpoint=config['endpoint']
    )
    
    if args.json:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        print(format_video_list(result))
    
    sys.exit(0 if result.get('code') == 2000 else 1)


if __name__ == '__main__':
    main()


path: scripts/local_playback_url.py
content: #!/usr/bin/env python3
"""
获取本地录像回放或下载地址脚本
"""

import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError


def generate_timestamp():
    """生成 20 位时间戳（毫秒）"""
    return str(int(time.time() * 1000)).zfill(20)


def str2byte(s):
    """字符串转字节数组"""
    return list(s.encode('utf-8'))


def change(encrypt_str, move_card):
    """移位算法"""
    encrypt_byte = str2byte(encrypt_str)
    length = len(encrypt_byte)
    for idx in range(length):
        tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
        encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
    return encrypt_byte


def merge_byte(encrypt_byte, change_byte):
    """合并字节数组"""
    length = len(encrypt_byte)
    temp = [0] * (length * 2)
    for idx in range(length):
        temp[idx] = encrypt_byte[idx]
        temp[length * 2 - 1 - idx] = change_byte[idx]
    return temp


def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
    """生成杰峰 API 签名"""
    encrypt_str = uuid + app_key + app_secret + time_millis
    encrypt_byte = str2byte(encrypt_str)
    change_byte = change(encrypt_str, movecard)
    merged_byte = merge_byte(encrypt_byte, change_byte)
    return hashlib.md5(bytes(merged_byte)).hexdigest()


def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
    """通过设备序列号生成 deviceToken"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/token"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    body = {"sns": [sn], "accessToken": ""}
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except Exception as e:
        return {"error": str(e)}


def get_local_playback_url(device_token, uuid, app_key, app_secret, movecard=5,
                           channel=0, stream_type=1, protocol="hls-ts",
                           start_time=None, end_time=None, file_name=None,
                           username="admin", password="", download=False,
                           endpoint="api.jftechws.com"):
    """获取本地录像回放或下载地址"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/playbackUrl/{device_token}"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    
    body = {
        "channel": channel,
        "streamType": stream_type,
        "protocol": protocol,
        "startTime": start_time,
        "endTime": end_time,
        "fileName": file_name,
        "username": username,
        "password": password,
        "download": 1 if download else 0
    }
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except Exception as e:
        return {"error": str(e)}


def get_config_from_env():
    """从环境变量读取配置"""
    required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
    missing_vars = [var for var in required_vars if not os.environ.get(var)]
    if missing_vars:
        raise Exception(f"缺少必需的环境变量：{', '.join(missing_vars)}")
    
    return {
        'uuid': os.environ.get('JF_UUID'),
        'appkey': os.environ.get('JF_APPKEY'),
        'appsecret': os.environ.get('JF_APPSECRET'),
        'movecard': int(os.environ.get('JF_MOVECARD', 5)),
        'sn': os.environ.get('JF_SN'),
        'username': os.environ.get('JF_USERNAME', 'admin'),
        'password': os.environ.get('JF_PASSWORD', ''),
        'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
    }


def main():
    parser = argparse.ArgumentParser(description='获取本地录像回放或下载地址')
    parser.add_argument('--file-name', required=True, help='录像文件名')
    parser.add_argument('--start-time', required=True, help='回放开始时间')
    parser.add_argument('--stop-time', required=True, help='回放结束时间')
    parser.add_argument('--channel', type=int, default=0, help='设备通道号')
    parser.add_argument('--stream-type', type=int, choices=[0, 1], default=1, help='码流类型')
    parser.add_argument('--protocol', default='hls-ts', choices=['flv', 'flv-enhanced', 'hls-ts', 'hls-fmp4', 'mp4', 'rtsp-sdp', 'rtsp-pri'])
    parser.add_argument('--download', action='store_true', help='下载模式')
    
    args = parser.parse_args()
    
    try:
        config = get_config_from_env()
    except Exception as e:
        print(f'❌ 配置错误：{e}')
        sys.exit(1)
    
    print("=" * 70)
    print("🎬 获取本地录像回放地址")
    print("=" * 70)
    print(f"设备 SN: {config['sn']}")
    print(f"文件名：{args.file_name}")
    print(f"时间：{args.start_time} - {args.stop_time}")
    print(f"协议：{args.protocol}")
    print()
    
    print(">>> 获取设备 Token...")
    token_result = get_device_token(config['sn'], config['uuid'], config['appkey'], 
                                    config['appsecret'], config['movecard'], config['endpoint'])
    
    if token_result.get('error') or token_result.get('code') != 2000:
        print(f"❌ 获取设备 Token 失败：{token_result.get('error') or token_result.get('msg')}")
        sys.exit(1)
    
    device_token = token_result['data'][0]['token']
    print(f"✅ 设备 Token 获取成功")
    print()
    
    print(">>> 获取回放地址...")
    result = get_local_playback_url(
        device_token=device_token, uuid=config['uuid'], app_key=config['appkey'],
        app_secret=config['appsecret'], movecard=config['movecard'],
        channel=args.channel, stream_type=args.stream_type, protocol=args.protocol,
        start_time=args.start_time, end_time=args.stop_time, file_name=args.file_name,
        username=config['username'], [REDACTED_SECRET]'password'], download=args.download,
        endpoint=config['endpoint']
    )
    
    if result.get('error'):
        print(f"❌ 获取回放地址失败：{result['error']}")
        sys.exit(1)
    
    if result.get('code') != 2000:
        print(f"❌ API 错误码：{result.get('code')} - {result.get('msg')}")
        sys.exit(1)
    
    data = result.get('data', {})
    if data.get('Ret') != 100:
        print(f"❌ 设备错误码：{data.get('Ret')}")
        sys.exit(1)
    
    play_url = data.get('url')
    if not play_url:
        print("❌ 未找到播放 URL")
        sys.exit(1)
    
    print("✅ 回放地址获取成功！")
    print()
    print("=" * 70)
    print("🔗 播放地址")
    print("=" * 70)
    print(f"{play_url}")
    print()
    print("⚠️ 回放地址有效期 10 小时，同时只支持一路回放")
    print()
    # 仅输出必要信息，避免泄露完整响应数据
    print("=" * 70)
    print("📋 响应摘要")
    print("=" * 70)
    print()
    print(json.dumps({"code": result.get('code'), "msg": result.get('msg')}, indent=2, ensure_ascii=False))
    
    sys.exit(0)


if __name__ == '__main__':
    main()


path: scripts/local_video_list.py
content: #!/usr/bin/env python3
"""
获取本地录像回放列表脚本
"""

import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError


def generate_timestamp():
    """生成 20 位时间戳（毫秒）"""
    return str(int(time.time() * 1000)).zfill(20)


def str2byte(s):
    """字符串转字节数组"""
    return list(s.encode('utf-8'))


def change(encrypt_str, move_card):
    """移位算法"""
    encrypt_byte = str2byte(encrypt_str)
    length = len(encrypt_byte)
    for idx in range(length):
        tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
        encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
    return encrypt_byte


def merge_byte(encrypt_byte, change_byte):
    """合并字节数组"""
    length = len(encrypt_byte)
    temp = [0] * (length * 2)
    for idx in range(length):
        temp[idx] = encrypt_byte[idx]
        temp[length * 2 - 1 - idx] = change_byte[idx]
    return temp


def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
    """生成杰峰 API 签名"""
    encrypt_str = uuid + app_key + app_secret + time_millis
    encrypt_byte = str2byte(encrypt_str)
    change_byte = change(encrypt_str, movecard)
    merged_byte = merge_byte(encrypt_byte, change_byte)
    return hashlib.md5(bytes(merged_byte)).hexdigest()


def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
    """通过设备序列号生成 deviceToken"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/token"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    body = {"sns": [sn], "accessToken": ""}
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except Exception as e:
        return {"error": str(e)}


def get_local_video_list(device_token, start_time, stop_time, uuid, app_key, 
                         app_secret, movecard=5, channel=0, event="*", 
                         endpoint="api.jftechws.com"):
    """获取本地录像回放列表"""
    time_millis = generate_timestamp()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
    
    url = f"https://{endpoint}/gwp/v3/rtc/device/opdev/{device_token}"
    headers = {
        "uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
        "signature": signature, "Content-Type": "application/json"
    }
    
    body = {
        "Name": "OPFileQuery",
        "OPFileQuery": {
            "BeginTime": start_time,
            "EndTime": stop_time,
            "Channel": channel,
            "DriverTypeMask": "0x0000FFFF",
            "Event": event,
            "StreamType": "0x00000000",
            "Type": "h264"
        }
    }
    
    req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
    try:
        with urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
    except HTTPError as e:
        return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
    except Exception as e:
        return {"error": str(e)}


def get_config_from_env():
    """从环境变量读取配置"""
    required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
    missing_vars = [var for var in required_vars if not os.environ.get(var)]
    if missing_vars:
        raise Exception(f"缺少必需的环境变量：{', '.join(missing_vars)}")
    
    return {
        'uuid': os.environ.get('JF_UUID'),
        'appkey': os.environ.get('JF_APPKEY'),
        'appsecret': os.environ.get('JF_APPSECRET'),
        'movecard': int(os.environ.get('JF_MOVECARD', 5)),
        'sn': os.environ.get('JF_SN'),
        'username': os.environ.get('JF_USERNAME', 'admin'),
        'password': os.environ.get('JF_PASSWORD', ''),
        'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
    }


def format_video_list(result):
    """格式化本地录像列表输出"""
    if result.get('error'):
        return f"❌ 错误：{result['error']}"
    
    if result.get('code') != 2000:
        return f"❌ API 错误码：{result.get('code')}\n   详情：{result.get('msg', 'Unknown error')}"
    
    data = result.get('data', {})
    if data.get('Ret') != 100:
        return f"❌ 设备错误码：{data.get('Ret')}"
    
    videos = data.get('OPFileQuery', [])
    if not videos:
        return "📭 未找到匹配的录像文件"
    
    output = [f"✅ 找到 {len(videos)} 个录像文件", ""]
    
    for i, video in enumerate(videos, 1):
        output.append(f"📹 录像 {i}:")
        output.append(f"   时间：{video.get('BeginTime', 'N/A')} - {video.get('EndTime', 'N/A')}")
        output.append(f"   文件名：{video.get('FileName', 'N/A')}")
        file_length = video.get('FileLength', '0')
        if isinstance(file_length, str) and file_length.startswith('0x'):
            file_length_kb = int(file_length, 16) / 1024
        else:
            file_length_kb = int(file_length) / 1024 if file_length else 0
        output.append(f"   大小：{file_length_kb:.1f} MB")
        output.append("")
    
    return "\n".join(output)


def main():
    parser = argparse.ArgumentParser(description='获取本地录像回放列表')
    parser.add_argument('--start-time', required=True, help='开始时间（YYYY-mm-dd HH:MM:SS）')
    parser.add_argument('--stop-time', required=True, help='结束时间')
    parser.add_argument('--channel', type=int, default=0, help='设备通道号')
    parser.add_argument('--event', default='*', help='录像类型')
    parser.add_argument('--json', action='store_true', help='输出 JSON 格式')
    
    args = parser.parse_args()
    
    try:
        config = get_config_from_env()
    except Exception as e:
        print(f'❌ 配置错误：{e}')
        sys.exit(1)
    
    print("=" * 70)
    print("📋 获取本地录像回放列表")
    print("=" * 70)
    print(f"设备 SN: {config['sn']}")
    print(f"时间范围：{args.start_time} - {args.stop_time}")
    print(f"录像类型：{args.event}")
    print()
    
    print(">>> 获取设备 Token...")
    token_result = get_device_token(config['sn'], config['uuid'], config['appkey'], 
                                    config['appsecret'], config['movecard'], config['endpoint'])
    
    if token_result.get('error') or token_result.get('code') != 2000:
        print(f"❌ 获取设备 Token 失败：{token_result.get('error') or token_result.get('msg')}")
        sys.exit(1)
    
    device_token = token_result['data'][0]['token']
    print(f"✅ 设备 Token 获取成功")
    print()
    
    print(">>> 获取本地录像列表...")
    result = get_local_video_list(
        device_token=device_token, start_time=args.start_time, stop_time=args.stop_time,
        uuid=config['uuid'], app_key=config['appkey'], app_secret=config['appsecret'],
        movecard=config['movecard'], channel=args.channel, event=args.event,
        endpoint=config['endpoint']
    )
    
    if args.json:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        print(format_video_list(result))
    
    sys.exit(0 if result.get('code') == 2000 and result.get('data', {}).get('Ret') == 100 else 1)


if __name__ == '__main__':
    main()


path: .clawhub/origin.json
content: version: 1
registry: https://clawhub.ai
slug: jf-open-pro-video-record
installedVersion: 1.0.1
installedAt: 1775635200000

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)