@clawhub-jftech-5fd12bdbe6
JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
---
name: jf-open-pro-ai-smart-search
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
# 必需凭证声明 - 平台元数据
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_USER
type: string
description: 用户 ID
default: admin
- 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 # Python 脚本
network_access:
- api.jftechws.com # 杰峰官方 API (国际)
- api-cn.jftech.com # 杰峰官方 API (中国大陆)
file_access: none # 不读取本地文件
---
# JF Open Pro AI Smart Search
> **面向开发者杰峰 AI 智搜工具 (Python)**
>
> 根据语义内容搜索杰峰云存报警视频,获取匹配的视频片段列表及播放信息。
---
## 🔒 安全说明
**凭据存储:仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 推荐方式,避免凭据出现在进程列表或日志中 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免明文存储凭据 |
**网络访问:**
- ✅ 仅访问杰峰官方 API 端点 (`api.jftechws.com` / `api-cn.jftech.com`)
- ❌ 不访问第三方服务
- ❌ 不读取本地文件系统
**脚本行为:**
- ✅ 本地执行 Python 脚本(技能本身)
- ✅ 仅向指定的杰峰 API 端点发起 HTTPS 请求
- ❌ 不执行外部命令
- ❌ 不读取敏感系统文件
---
## 🚀 快速开始
### 设置环境变量
```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_USER="admin" # 用户 ID(可选,默认:admin)
```
### 使用技能
```bash
# AI 智搜 - 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# AI 智搜 - 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# AI 智搜 - 搜索"狗"相关的视频
python scripts/search_video.py --search "狗"
# AI 智搜 - 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
# 获取云存回放地址(指定时间)
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性 | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USER` | 用户 ID | 否 | `admin` |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
---
## 🛠️ 功能
### 1. AI 智搜视频
根据语义内容搜索 AI 标记的云存报警视频。
**支持的搜索类型:**
| 搜索类型 | 示例查询 | 说明 |
|----------|----------|------|
| 人物 | "人"、"戴帽子的人"、"穿红色衣服的人" | 基于人形 + 属性检测 |
| 车辆 | "车"、"白色轿车"、"卡车" | 基于车辆检测 |
| 动物 | "狗"、"猫" | 基于动物检测 |
| 行为 | "跑步的人"、"摔倒" | 基于行为分析 |
**使用示例:**
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
```
**返回字段说明:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `st` | 录像开始时间(秒) | 1703275200 |
| `et` | 录像结束时间(秒) | 1703275260 |
| `matchRate` | 匹配度(0-1) | 0.95 |
| `queryTags` | 检测到的标签列表 | ["person", "hat"] |
| `eventTime` | 事件触发时间 | "2024-12-23 10:00:00" |
---
### 2. 云存回放地址获取
获取云存报警视频回放/播放地址。
**使用示例:**
```bash
# 指定时间范围获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
**工作流程:**
```
1. AI 智搜搜索视频
↓
获取云存报警信息视频列表
↓
2. 选择目标视频
↓
提取 st(开始时间)和 et(结束时间)
↓
3. 调用云存报警视频回放 API
↓
st 对应 startTime
et 对应 stopTime
↓
4. 获取播放链接
```
---
## 📖 使用场景示例
### 场景 1: 搜索特定人员的活动记录
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 查看返回结果,选择感兴趣的视频片段
# 使用返回的 st 和 et 获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
```
### 场景 2: 搜索车辆进出记录
```bash
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
```
### 场景 3: 完整流程 - 搜索并播放
```bash
# 一步完成:搜索"人"并获取第一个视频的回放地址
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `12504` | 授权失败 - 设备未开通 AI 智搜套餐 | 登录开放平台为设备绑定 AI 智搜套餐卡 |
| `10001` | 参数错误 | 检查请求参数格式 |
| `10002` | 签名失败 | 检查 appKey/appSecret 和时间戳 |
### 错误码 12504 处理
**错误信息:** `authorize failed, Please check it in the open platform`
**原因:** 设备未开通 AI 智搜服务,或未绑定套餐卡
**解决步骤:**
1. 登录杰峰开放平台:https://developer.jftech.com
2. 进入 **套餐管理** / **服务管理**
3. 找到 **AI 智搜** 或 **云存视频搜索** 套餐
4. 为设备购买并绑定套餐卡
5. 等待配置生效(通常 1-5 分钟)
6. 重新调用 API 测试
---
## ⚠️ 注意事项
1. **设备需开通云存服务** - AI 智搜需要云存套餐支持
2. **设备需开通 AI 智搜套餐** - 需在开放平台绑定套餐卡
3. **时间范围** - 只能搜索云存有效期内的视频
4. **搜索精度** - 受 AI 算法识别精度影响
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **AI 智搜文档**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=d2c0d9105d9c4b78bc0d2ee3851d2557
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
**可用脚本:**
| 脚本 | 功能 |
|------|------|
| `search_video.py` | AI 智搜 - 搜索云存报警视频 |
| `get_playback_url.py` | 获取云存回放地址(指定时间或完整流程) |
| `ai_search_playback.py` | 完整流程 - AI 智搜 + 播放地址一键获取 |
```bash
# 获取帮助
python scripts/search_video.py --help
python scripts/get_playback_url.py --help
python scripts/ai_search_playback.py --help
# AI 智搜
python scripts/search_video.py --search <搜索内容>
# 获取回放地址(指定时间)
python scripts/get_playback_url.py --start-time "YYYY-MM-DD HH:MM:SS" --stop-time "YYYY-MM-DD HH:MM:SS"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search <搜索内容> --video-index <索引>
```
脚本路径:`scripts/search_video.py`, `scripts/get_playback_url.py`, `scripts/ai_search_playback.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:skill.yaml
# JF Open Pro AI Smart Search - Skill Registry Metadata
# This file defines the skill's requirements for ClawHub registry
name: jf-open-pro-ai-smart-search
version: 1.0.0
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。
# 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/
- name: JF_SN
description: 设备序列号
source: 杰峰设备机身标签或管理后台
# Optional environment variables
optionalEnvVars:
- name: JF_USER
description: 用户 ID
default: admin
- 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: search_video.py
description: AI 智搜 - 搜索云存报警视频
entryPoint: scripts/search_video.py
- name: get_playback_url.py
description: 获取云存回放地址
entryPoint: scripts/get_playback_url.py
- name: ai_search_playback.py
description: 完整流程 - AI 智搜 + 播放地址
entryPoint: scripts/ai_search_playback.py
# Tags for discovery
tags:
- jf-tech
- 杰峰
- ai-search
- video-search
- cloud-storage
- 云存搜索
FILE:scripts/ai_search_playback.py
#!/usr/bin/env python3
"""
AI 智搜 + 云存回放完整流程脚本
工作流程:
1. 调用 AI 智搜 API 获取视频列表
2. 选择指定索引的视频
3. 提取开始/结束时间
4. 调用云存回放 API 获取播放地址
用法:
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"
export JF_USER="admin"
python ai_search_playback.py --search "人" --video-index 0
"""
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_jftech_sign(appkey: str, secret: str, timestamp: int) -> str:
"""生成 JF Tech API 签名"""
sign_str = f"{appkey}{timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str = "") -> dict:
"""调用 JF Tech AI 智搜 API"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""生成杰峰 API 签名"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
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 = {"deviceSnList": [sn]}
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 {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def get_playback_url(sn, user, start_time, stop_time, uuid, app_key, app_secret, movecard=0,
endpoint="api.jftechws.com", stream_type="hls"):
"""获取云存回放地址"""
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error') or token_result.get('code') != 2000:
return {"error": f"获取 Token 失败:{token_result.get('error') or token_result.get('msg')}"}
if not token_result.get('data') or len(token_result['data']) == 0:
return {"error": "获取 Token 失败:返回数据为空"}
device_token = token_result['data'][0]['token']
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"user": user,
"sn": sn,
"startTime": start_time,
"stopTime": stop_time,
"streamType": stream_type
}
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 {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": 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'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存回放完整流程',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 搜索"人"并获取第一个视频的回放地址
python ai_search_playback.py --search "人" --video-index 0
# 搜索"车"并获取第二个视频的回放地址
python ai_search_playback.py --search "车" --video-index 1
''')
parser.add_argument('--search', required=True, help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print('========================================')
print('AI 智搜 + 云存回放完整流程')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"搜索内容:{args.search}")
print(f"视频索引:{args.video_index}")
print()
# 步骤 1: AI 智搜
print('>>> 步骤 1/3: AI 智搜搜索视频...')
search_result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.search,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['appsecret']
)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
if search_result.get('message'):
print(f" 详情:{search_result['message']}")
sys.exit(1)
if search_result.get('code') != 2000:
print(f"❌ API 错误码:{search_result.get('code')}")
print(f" 详情:{search_result.get('msg', 'Unknown error')}")
sys.exit(1)
data = search_result.get('data', {})
videos = data.get('videos', [])
if not videos:
print('❌ 未找到匹配的视频')
sys.exit(1)
print(f"✅ 找到 {len(videos)} 个匹配的视频")
if args.video_index >= len(videos):
print(f"❌ 视频索引 {args.video_index} 超出范围 (0-{len(videos)-1})")
sys.exit(1)
video = videos[args.video_index]
print(f" 选择:片段 {args.video_index + 1}")
print(f" 时间:{video.get('eventTime', 'N/A')}")
print(f" 匹配度:{video.get('matchRate', 0):.0%}")
print()
# 步骤 2: 提取时间
start_time = video.get('st')
stop_time = video.get('et')
if not start_time or not stop_time:
print('❌ 无法提取视频时间信息')
sys.exit(1)
# 转换时间戳为可读格式
from datetime import datetime
start_dt = datetime.fromtimestamp(start_time)
stop_dt = datetime.fromtimestamp(stop_time)
print('>>> 步骤 2/3: 提取视频时间...')
print(f" 开始:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} ({start_time})")
print(f" 结束:{stop_dt.strftime('%Y-%m-%d %H:%M:%S')} ({stop_time})")
print()
# 步骤 3: 获取回放地址
print('>>> 步骤 3/3: 获取云存回放地址...')
playback_result = get_playback_url(
sn=config['sn'],
user=config['user'],
start_time=start_time,
stop_time=stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error'):
print(f"❌ 获取回放地址失败:{playback_result['error']}")
sys.exit(1)
if playback_result.get('code') != 2000:
print(f"❌ API 错误码:{playback_result.get('code')}")
print(f" 详情:{playback_result.get('msg', 'Unknown error')}")
sys.exit(1)
playback_data = playback_result.get('data', {})
playback_url = playback_data.get('url') or playback_data.get('playUrl')
if not playback_url:
print('❌ 未找到播放 URL')
print(json.dumps(playback_result, indent=2, ensure_ascii=False))
sys.exit(1)
print('✅ 回放地址获取成功')
print()
print('========================================')
print('播放信息')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"时间范围:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} - {stop_dt.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"播放地址:{playback_url}")
print()
print("使用方式:")
print(f" - VLC 播放:vlc \"{playback_url}\"")
print(f" - 网页播放:在浏览器中打开 URL")
print(f" - 下载:curl -o video.mp4 \"{playback_url}\"")
print('========================================')
if __name__ == "__main__":
main()
FILE:scripts/get_playback_url.py
#!/usr/bin/env python3
"""
云存报警视频回放地址获取脚本
工作流程:
1. 先通过 AI 智搜获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
API 端点:
1. 获取设备 Token: POST https://api.jftechws.com/gwp/v3/rtc/device/token
2. 云存回放:POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
用法:
# 设置环境变量(必需)
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn"
export JF_USER="admin" # 可选,默认 admin
export JF_ENDPOINT="api.jftechws.com" # 可选
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-03-28 15:23:26" --stop-time "2026-03-28 15:23:36"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""
生成杰峰 API 签名
签名算法:MD5(uuid + appKey + timeMillis + secret)
Args:
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
time_millis: 时间戳(毫秒),已包含 movecard 偏移
"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
通过设备序列号生成 deviceToken
API: POST https://api.jftechws.com/gwp/v3/rtc/device/token
Args:
sn: 设备序列号
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应,包含 deviceToken
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
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 = {"deviceSnList": [sn]}
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_playback_url(device_token, sn, user, start_time, stop_time,
uuid, app_key, app_secret, movecard=0,
channel=0, stream_type=1, endpoint="api.jftechws.com"):
"""
获取云存报警视频回放或下载地址
根据录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
获取对应云存报警视频播放链接
API: POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
Args:
device_token: 设备 Token(从 get_device_token 获取)
sn: 设备序列号
user: 用户 ID
start_time: 录像开始时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 st 字段)
stop_time: 录像结束时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 et 字段)
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
channel: 通道号(默认 0)
stream_type: 码流类型(1=辅码流,2=主码流,默认 1)
endpoint: API 端点
Returns:
dict: API 响应,包含播放地址
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
# 云存回放 API 端点
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"
}
# 请求体:startTime 对应 st,stopTime 对应 et
body = {
"sn": sn,
"user": user,
"startTime": start_time,
"stopTime": stop_time,
"channel": channel,
"streamType": stream_type
}
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 ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
AI 智搜 - 搜索云存报警视频
API: POST https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
url = f"https://{endpoint}/aisvr/v3/gateway/api/viewsearch/searchVideo"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"sn": sn,
"user": user,
"searchContent": search_content
}
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}"}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def ai_search_and_playback(sn, user, search_content, uuid, app_key, app_secret, movecard=0,
video_index=0, endpoint="api.jftechws.com"):
"""
完整流程:AI 智搜 + 云存报警视频回放地址获取
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
video_index: 选择第几个视频(从 0 开始)
endpoint: API 端点
Returns:
dict: 包含搜索结果和播放地址
"""
print("=" * 70)
print("🎬 AI 智搜 + 云存报警视频回放地址获取")
print("=" * 70)
print()
print(f"设备 SN: {sn}")
print(f"用户:{user}")
print(f"搜索内容:{search_content}")
print(f"选择视频索引:{video_index}")
print()
# 步骤 1: AI 智搜 - 获取云存报警信息视频列表
print(">>> 步骤 1: 搜索视频获取云存报警信息视频列表...")
search_result = ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard, endpoint)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
return {"error": search_result['error']}
if search_result.get('code') != 2000:
print(f"❌ AI 智搜失败:{search_result.get('msg', 'Unknown error')}")
return {"error": search_result.get('msg', 'Unknown error')}
videos = search_result.get('data', [])
if not videos:
print("❌ 未找到匹配的视频")
return {"error": "No videos found"}
print(f"✅ AI 智搜成功,找到 {len(videos)} 条视频")
print()
# 选择指定索引的视频
if video_index >= len(videos):
print(f"❌ 视频索引 {video_index} 超出范围(0-{len(videos)-1})")
return {"error": f"Video index {video_index} out of range"}
video = videos[video_index]
print(f"📹 选择第 {video_index + 1} 个视频:")
print(f" 录像开始时间(st):{video['st']}")
print(f" 录像结束时间(et):{video['et']}")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
# 步骤 2: 通过设备序列号生成 deviceToken
print(">>> 步骤 2: 通过设备序列号生成 deviceToken...")
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error'):
print(f"❌ 获取 deviceToken 失败:{token_result['error']}")
return {"error": token_result['error'], "search_result": search_result}
if token_result.get('code') != 2000:
print(f"❌ 获取 deviceToken 失败:{token_result.get('msg', 'Unknown error')}")
return {"error": token_result.get('msg', 'Unknown error'), "search_result": search_result}
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功:{device_token[:30]}...")
print()
# 步骤 3: 获取云存报警视频回放地址
print(">>> 步骤 3: 获取云存报警视频回放地址...")
print(f" API 端点:POST /gwp/v3/rtc/device/getVideoUrl/{device_token[:30]}...")
print(f" startTime: {video['st']} (对应 st)")
print(f" stopTime: {video['et']} (对应 et)")
print()
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=sn,
user=user,
start_time=video['st'], # st 对应 startTime
stop_time=video['et'], # et 对应 stopTime
uuid=uuid,
app_key=app_key,
app_secret=app_secret,
endpoint=endpoint
)
if playback_result.get('error'):
print(f"❌ 获取播放地址失败:{playback_result['error']}")
return {"error": playback_result['error'], "search_result": search_result}
if playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('msg', 'Unknown error')}")
return {"error": playback_result.get('msg', 'Unknown error'), "search_result": search_result}
# 成功获取播放地址
play_url = playback_result['data'].get('url')
print("✅ 云存报警视频播放地址获取成功!")
print()
print("=" * 70)
print("🎬 播放地址")
print("=" * 70)
print()
print(f"📹 视频信息:")
print(f" 时间:{video['st']} - {video['et']}")
print(f" 时长:10 秒")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
print(f"🔗 播放地址:")
print(f" {play_url}")
print()
print("=" * 70)
print("🎯 播放方式:")
print("=" * 70)
print()
print("1. VLC 播放器:")
print(f' vlc "{play_url}"')
print()
print("2. 网页播放(HLS.js):")
print(f' <video src="{play_url}" controls></video>')
print()
print("3. FFmpeg 下载:")
print(f' ffmpeg -i "{play_url}" -c copy video.mp4')
print()
return {
"success": True,
"search_result": search_result,
"playback_result": playback_result,
"video_info": video,
"play_url": play_url
}
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'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存报警视频回放地址获取',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
使用流程:
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
'''
)
parser.add_argument('--search', help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
parser.add_argument('--start-time', help='录像开始时间(格式:YYYY-MM-DD HH:MM:SS)')
parser.add_argument('--stop-time', help='录像结束时间(格式:YYYY-MM-DD HH:MM:SS)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
# 如果有 search 参数,执行完整流程
if args.search:
result = ai_search_and_playback(
sn=config['sn'],
user=config['user'],
search_content=args.search,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
video_index=args.video_index,
endpoint=config['endpoint']
)
# 如果有 start_time 和 stop_time 参数,直接获取播放地址
elif args.start_time and args.stop_time:
print(">>> 通过设备序列号生成 deviceToken...")
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"❌ 获取 deviceToken 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功")
print(">>> 获取云存报警视频回放地址...")
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=config['sn'],
user=config['user'],
start_time=args.start_time,
stop_time=args.stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error') or playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('error') or playback_result.get('msg')}")
sys.exit(1)
play_url = playback_result['data'].get('url')
print(f"✅ 播放地址:{play_url}")
result = {"success": True, "play_url": play_url}
else:
parser.print_help()
sys.exit(1)
# 输出 JSON 结果
print()
print("=" * 70)
print("📋 JSON 结果")
print("=" * 70)
print()
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result.get('success') else 1)
if __name__ == '__main__':
main()
FILE:scripts/search_video.py
#!/usr/bin/env python3
"""
AI 智搜脚本 - 搜索云存报警视频
仅支持环境变量配置凭据,避免命令行泄露风险。
支持平台:JF Tech(杰峰)
用法:
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"
export JF_USER="admin"
python search_video.py --search "人"
python search_video.py --search "车"
python search_video.py --search "戴帽子的人"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_jftech_sign(appkey: str, secret: str, timestamp: int, movecard: int = 0) -> str:
"""
生成 JF Tech API 签名
Args:
appkey: 应用 appKey
secret: 应用密钥
timestamp: 时间戳(毫秒)
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
# 时间戳加上 movecard 偏移量
adjusted_timestamp = timestamp + movecard
sign_str = f"{appkey}{adjusted_timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str, movecard: int = 0) -> dict:
"""
调用 JF Tech AI 智搜 API
Args:
sn: 设备序列号
user: 用户 ID
query: 搜索内容(语义描述)
uuid: 开放平台用户 uuid
appkey: 应用 appKey
secret: 应用密钥
authorization: 用户 token
movecard: 签名算法偏移量 (0-9)
Returns:
API 响应字典
"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp, movecard)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def format_results(results: dict) -> str:
"""格式化搜索结果输出"""
if "error" in results:
return f"❌ 错误:{results.get('error', 'Unknown')}\n{results.get('message', '')}"
if results.get("code") != 2000:
return f"❌ API 错误码:{results.get('code')}\n{results.get('msg', '')}"
data = results.get("data", {})
videos = data.get("videos", [])
if not videos:
return "📭 未找到匹配的视频"
output = []
output.append(f"✅ 找到 {len(videos)} 个匹配的视频片段\n")
for i, video in enumerate(videos, 1):
output.append(f"📹 片段 {i}:")
output.append(f" 时间:{video.get('eventTime', 'N/A')}")
output.append(f" 匹配度:{video.get('matchRate', 0):.0%}")
output.append(f" 标签:{', '.join(video.get('queryTags', []))}")
output.append(f" 大小:{video.get('vidsz', 0) / 1024:.1f} KB")
if video.get('picfg') == 1:
output.append(f" 缩略图:有")
output.append("")
return "\n".join(output)
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'),
'secret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description="AI 智搜 - 搜索云存报警视频",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 搜索"人"相关的视频
python search_video.py --search "人"
# 搜索"车"相关的视频
python search_video.py --search "车"
# 搜索"戴帽子的人"
python search_video.py --search "戴帽子的人"
''')
parser.add_argument("--search", dest="query", required=True, 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)
result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.query,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['secret'],
authorization='', # 如需 authorization 可从环境变量添加
movecard=config['movecard']
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_results(result))
if __name__ == "__main__":
main()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-ai-smart-search",
"installedVersion": "1.0.3",
"installedAt": 1775547600000
}
面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
---
name: jf-open-pro-ptz-control
description: 面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
# 必需凭证声明 - 平台元数据
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 # 仅支持 Python
---
# JF Open Pro PTZ Control
> **面向开发者杰峰设备云台控制工具 (Python)**
>
> 支持设备状态查询、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```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" # 设备密码(可选)
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽(开启)
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 一键遮蔽(关闭)
python scripts/jf_open_pro_ptz_control.py mask --enable false
# 变倍控制(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
# 添加巡航点
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 获取巡航列表
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `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. 设备状态查询
查询设备在线状态、休眠状态、认证状态、设备 WAN IP 等。
```bash
python scripts/jf_open_pro_ptz_control.py status
```
**返回信息:**
- 设备在线状态(online/notfound)
- 低功耗设备休眠状态
- 认证状态
- 设备 WAN IP
---
### 2. 方向控制 (PTZ)
云台支持 8 个方向转动:
| 方向 | 参数值 |
|------|--------|
| 上 | `up` |
| 下 | `down` |
| 左 | `left` |
| 右 | `right` |
| 左上 | `leftup` |
| 左下 | `leftdown` |
| 右上 | `rightup` |
| 右下 | `rightdown` |
**使用示例:**
```bash
# 开始向上转动(速度 5)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start --step 5
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
```
**参数说明:**
- `--direction`: 方向(up/down/left/right/leftup/leftdown/rightup/rightdown)
- `--action`: 动作(start/stop)
- `--step`: 速度(1-8,1 最慢,8 最快,默认:5)
⚠️ **重要**: 必须先发送 start 再发送 stop,建议间隔 500ms。如果不发送 stop,设备会一直转动到最大角度。
---
### 3. 一键遮蔽 (Mask)
开启后摄像头转至最下方然后转至最右侧,同时关闭视频预览和录像。
```bash
# 开启遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 关闭遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable false
```
---
### 4. 变倍和聚焦控制 (Zoom/Focus)
支持变倍(Zoom)和聚焦(Focus)操作:
| 功能 | 参数值 | 说明 |
|------|--------|------|
| 变倍 - | `ZoomWide` | 缩小(广角) |
| 变倍 + | `ZoomTile` | 放大(长焦) |
| 聚焦 - | `FocusFar` | 聚焦远处 |
| 聚焦 + | `FocusNear` | 聚焦近处 |
| 光圈 - | `IrisSmall` | 缩小光圈 |
| 光圈 + | `IrisLarge` | 放大光圈 |
**使用示例:**
```bash
# 开始变倍 +(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start --step 8
# 停止
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
```
---
### 5. 预置位管理 (Preset)
预置点编号范围:1-255(建议不使用 200 以后的编号)
**特殊预置点:**
- `100`: 移动追踪守望位(追踪停止后自动回归)
- `128`: 自检回归预置点(设备重启或自检时回归)
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 设置预置点 | `set` | 将当前位置保存为预置点 |
| 删除预置点 | `clear` | 删除指定预置点 |
| 转到预置点 | `goto` | 云台转动到预置点位置 |
| 编辑预置点名 | `name` | 修改预置点名称 |
| 获取列表 | `list` | 获取所有预置点 |
**使用示例:**
```bash
# 设置预置点 1,名称为"门口"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 删除预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command clear --id 1
# 编辑预置点名称
python scripts/jf_open_pro_ptz_control.py preset --preset-command name --id 1 --name "新名称"
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
```
---
### 6. 巡航计划管理 (Tour)
巡航功能让设备在多个预置点之间自动循环巡视。
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 添加巡航点 | `add` | 往巡航线路添加预置点 |
| 删除巡航点 | `delete` | 从巡航线路删除预置点 |
| 启动巡航 | `start` | 开始自动巡航 |
| 停止巡航 | `stop` | 停止巡航 |
| 清除巡航线路 | `clear` | 清空整个巡航线路 |
| 获取列表 | `list` | 获取巡航配置 |
**使用示例:**
```bash
# 添加预置点 1 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1 --step 5
# 添加预置点 2 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
# 启动巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 停止巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command stop --tour-id 0
# 获取巡航配置
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📖 使用场景示例
### 场景 1: 基础云台控制
```bash
# 1. 检查设备状态
python scripts/jf_open_pro_ptz_control.py status
# 2. 向上转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 3. 向右转动
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action stop
```
### 场景 2: 设置并使用预置位
```bash
# 1. 转动到目标位置
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 2. 保存为预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 3. 转动到其他位置...
# 4. 回到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
```
### 场景 3: 设置自动巡航
```bash
# 1. 设置多个预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "位置 1"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 2 --name "位置 2"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 3 --name "位置 3"
# 2. 添加到巡航线路
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 3
# 3. 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 脚本会自动处理登录 |
| `526` | 低电量/不支持 | 设备电量不足或为固定摄像头 |
### 错误码 526 说明
**含义:** 设备支持云台,但电量过低无法执行
**解决方案:**
1. 给设备充电
2. 等待电量恢复至 20% 以上
3. 使用电源供电模式
---
## ⚠️ 注意事项
1. **设备需在线** - 操作前确保设备在线
2. **设备需登录** - 脚本会自动处理设备登录
3. **PTZ 控制** - start/stop 指令需串行发送(间隔 500ms)
4. **预置点范围** - 建议使用 1-199 编号
5. **电量检查** - 低电量时云台功能可能被禁用
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
```bash
# 获取帮助
python scripts/jf_open_pro_ptz_control.py --help
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# PTZ 方向控制
python scripts/jf_open_pro_ptz_control.py ptz --direction <方向> --action <start|stop>
# 一键遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable <true|false>
# 变倍聚焦
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command <命令> --action <start|stop>
# 预置点管理
python scripts/jf_open_pro_ptz_control.py preset --preset-command <set|clear|goto|name|list> [选项]
# 巡航管理
python scripts/jf_open_pro_ptz_control.py tour --tour-command <add|delete|start|stop|clear|list> [选项]
```
脚本路径:`scripts/jf_open_pro_ptz_control.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:scripts/jf_open_pro_ptz_control.py
#!/usr/bin/env python3
"""
JF 杰峰云台 PTZ 控制工具 - Python 版本
仅支持环境变量配置凭据,避免命令行泄露风险。
用法:
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"
export JF_USERNAME="admin"
export JF_PASSWORD="your-password"
python jf_open_pro_ptz_control.py status
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py mask --enable true
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
"""
import argparse
import hashlib
import json
import os
import time
import urllib.request
import urllib.error
# ==================== 工具函数 ====================
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 get_signature(uuid, app_key, app_secret, time_millis, move_card=5):
"""获取签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, move_card)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_time_millis():
"""获取 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def generate_request_id():
"""生成 32 位请求 ID"""
import random
return ''.join(random.choice('0123456789abcdef') for _ in range(32))
# ==================== HTTP 请求 ====================
def https_post(url, data, headers):
"""发送 HTTPS POST 请求"""
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
headers=headers,
method='POST'
)
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
raise Exception(f'HTTP 错误:{e.code} - {e.reason}')
except urllib.error.URLError as e:
raise Exception(f'请求失败:{e.reason}')
# ==================== 认证相关 ====================
def get_device_token(config):
"""获取设备 Token"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/token"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'sns': [config['deviceSn']], 'accessToken': ''}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取 Token 失败:{response.get('msg')} (code: {response.get('code')})")
if not response.get('data') or len(response['data']) == 0:
raise Exception('获取 Token 失败:返回数据为空')
return response['data'][0]['token']
def device_login(config, device_token, keepalive_time=300):
"""设备登录"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'UserName': config['userName'],
'PassWord': config['passWord'] or '',
'KeepaliveTime': keepalive_time
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设备登录失败:{response.get('msg')} (code: {response.get('code')})")
if response.get('data', {}).get('Ret') != 100:
raise Exception(f"设备登录失败:设备返回码 {response['data']['Ret']}")
return response['data']
# ==================== 设备状态查询 ====================
def get_device_status(config):
"""获取设备状态"""
print('>>> 获取设备 Token...')
device_token = get_device_token(config)
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/status"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'deviceTokenList': [device_token], 'region': 'Local'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"查询状态失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'][0]
# ==================== PTZ 方向控制 ====================
DIRECTION_MAP = {
'up': 'DirectionUp',
'down': 'DirectionDown',
'left': 'DirectionLeft',
'right': 'DirectionRight',
'leftup': 'DirectionLeftUp',
'leftdown': 'DirectionLeftDown',
'rightup': 'DirectionRightUp',
'rightdown': 'DirectionRightDown'
}
def ptz_control(config, device_token, direction, action, step=5, channel=0):
"""云台方向控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command = DIRECTION_MAP.get(direction.lower())
if not command:
raise Exception(f'无效的方向:{direction}')
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Preset': preset,
'Channel': channel,
'Step': step
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"PTZ 控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 一键遮蔽 ====================
def set_mask(config, device_token, enable):
"""一键遮蔽"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'Name': 'General.OneKeyMaskVideo',
'General.OneKeyMaskVideo': [{'Enable': enable}]
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"一键遮蔽设置失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 变倍聚焦控制 ====================
def zoom_focus_control(config, device_token, command, action, step=8, channel=0):
"""变倍聚焦控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Channel': channel,
'Step': step,
'Preset': preset
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"变倍聚焦控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 预置点管理 ====================
def preset_control(config, device_token, command, preset_id, preset_name='', channel=0):
"""预置点控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'set': 'SetPreset',
'clear': 'ClearPreset',
'goto': 'GotoPreset',
'name': 'SetPresetName'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的预置点命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Preset': preset_id,
'Channel': channel
}
}
}
if command in ['set', 'name']:
data['OPPTZControl']['Parameter']['PresetName'] = preset_name or f'预置点{preset_id}'
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"预置点操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_preset_list(config, device_token):
"""获取预置点列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZPreset'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取预置点列表失败:{response.get('msg')} (code: {response.get('code')})")
# API 返回的是二维数组,需要扁平化
presets = response['data'].get('Uart.PTZPreset', [])
return [item for sublist in presets for item in sublist] if presets else []
# ==================== 巡航管理 ====================
def tour_control(config, device_token, command, tour_id=0, preset_id=0, step=5, channel=0):
"""巡航控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'add': 'AddTour',
'delete': 'DeleteTour',
'start': 'StartTour',
'stop': 'StopTour',
'clear': 'ClearTour'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的巡航命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Tour': tour_id,
'Channel': channel
}
}
}
if command in ['add', 'delete']:
data['OPPTZControl']['Parameter']['Preset'] = preset_id
data['OPPTZControl']['Parameter']['Step'] = step
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"巡航操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_tour_list(config, device_token):
"""获取巡航列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZTour'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取巡航列表失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'].get('Uart.PTZTour', [])
# ==================== 命令行参数解析 ====================
def parse_args():
parser = argparse.ArgumentParser(
description='JLink 杰峰云台 PTZ 控制工具 - Python 版本',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USERNAME 设备用户名,默认 admin (可选)
JF_PASSWORD 设备密码 (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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 jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽
python jf_open_pro_ptz_control.py mask --enable true
# 变倍控制(放大)
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 添加巡航点
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
''')
parser.add_argument('command', choices=['status', 'ptz', 'mask', 'zoom', 'preset', 'tour'], help='命令')
# PTZ 参数
parser.add_argument('--direction', choices=['up', 'down', 'left', 'right', 'leftup', 'leftdown', 'rightup', 'rightdown'], help='方向')
parser.add_argument('--action', choices=['start', 'stop'], help='动作')
parser.add_argument('--step', type=int, default=5, help='速度 1-8(默认:5)')
# Mask 参数
parser.add_argument('--enable', type=str, help='开启/关闭遮蔽 (true/false)')
# Zoom 参数
parser.add_argument('--zoom-command', dest='zoom_command', help='变倍/聚焦命令')
# Preset 参数
parser.add_argument('--preset-command', dest='preset_command', help='预置点操作 (set/clear/goto/name/list)')
parser.add_argument('--id', type=int, help='预置点 ID')
parser.add_argument('--name', help='预置点名称')
# Tour 参数
parser.add_argument('--tour-command', dest='tour_command', help='巡航操作 (add/delete/start/stop/clear/list)')
parser.add_argument('--tour-id', dest='tour_id', type=int, default=0, help='巡航线路 ID(默认:0)')
parser.add_argument('--preset-id', dest='preset_id', type=int, help='预置点 ID')
return parser.parse_args()
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)),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com'),
'deviceSn': os.environ.get('JF_SN'),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', '')
}
def main():
args = parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
return 1
try:
if args.command == 'status':
print('========================================')
print('JLink 设备状态查询')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print()
status = get_device_status(config)
print('=== 设备状态 ===')
print(f"设备序列号:{status.get('uuid')}")
print(f"状态:{status.get('status')}")
print(f"认证状态:{status.get('authStatus')}")
if status.get('wakeUpStatus') is not None:
print(f"唤醒状态:{status.get('wakeUpStatus')}")
if status.get('wakeUpEnable') is not None:
print(f"支持唤醒:{status.get('wakeUpEnable')}")
if status.get('wanIp'):
print(f"WAN IP: {status.get('wanIp')}")
elif args.command == 'ptz':
if not args.direction or not args.action:
print('❌ PTZ 命令需要 --direction 和 --action 参数')
return 1
print('========================================')
print('JLink 云台方向控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"方向:{args.direction}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = ptz_control(config, device_token, args.direction, args.action, args.step)
print('✅ PTZ 控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'mask':
if args.enable is None:
print('❌ Mask 命令需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
print('========================================')
print('JLink 一键遮蔽')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"开启遮蔽:{enable}")
print()
device_token = get_device_token(config)
result = set_mask(config, device_token, enable)
print(f"✅ 一键遮蔽{'开启' if enable else '关闭'}成功")
print(f"返回码:{result.get('Ret')}")
elif args.command == 'zoom':
if not args.zoom_command or not args.action:
print('❌ Zoom 命令需要 --zoom-command 和 --action 参数')
return 1
print('========================================')
print('JLink 变倍聚焦控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{args.zoom_command}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = zoom_focus_control(config, device_token, args.zoom_command, args.action, args.step)
print('✅ 变倍聚焦控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'preset':
if not args.preset_command:
print('❌ Preset 命令需要 --preset-command 参数')
return 1
print('========================================')
print('JLink 预置点管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.preset_command}")
if args.id is not None:
print(f"预置点 ID: {args.id}")
if args.name:
print(f"名称:{args.name}")
print()
device_token = get_device_token(config)
if args.preset_command == 'list':
presets = get_preset_list(config, device_token)
print('=== 预置点列表 ===')
if not presets:
print('暂无预置点')
else:
for p in presets:
print(f" ID {p.get('Id')}: {p.get('PresetName')}")
else:
if args.id is None:
print('❌ 需要指定 --id 参数')
return 1
result = preset_control(config, device_token, args.preset_command, args.id, args.name or '')
print(f'✅ 预置点{args.preset_command}成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'tour':
if not args.tour_command:
print('❌ Tour 命令需要 --tour-command 参数')
return 1
print('========================================')
print('JLink 巡航管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.tour_command}")
print(f"巡航线路 ID: {args.tour_id}")
if args.preset_id is not None:
print(f"预置点 ID: {args.preset_id}")
print()
device_token = get_device_token(config)
if args.tour_command == 'list':
tours = get_tour_list(config, device_token)
print('=== 巡航线路列表 ===')
print(json.dumps(tours, indent=2, ensure_ascii=False))
else:
result = tour_control(config, device_token, args.tour_command, args.tour_id, args.preset_id or 0, args.step)
print(f'✅ 巡航{args.tour_command}成功')
print(f"返回码:{result.get('Ret')}")
print('========================================')
return 0
except Exception as e:
print(f'❌ 错误:{e}')
return 1
if __name__ == '__main__':
exit(main())
面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
---
name: jf-open-pro-capture-livestream
description: 面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
# 必需凭证声明 - 平台元数据
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_USERNAME
type: string
description: 设备用户名
default: admin
- name: JF_PASSWORD
type: string
description: 设备密码
- name: JF_SN
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 # 仅支持 Python
---
# JF Open Pro Capture Livestream
> **面向开发者杰峰设备 API 工具 (Python)**
>
> 支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```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" # 设备序列号
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址
python scripts/jf_open_pro_capture_livestream.py livestream
# 获取 Token
python scripts/jf_open_pro_capture_livestream.py token
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USERNAME` | 设备用户名 | 否 | `admin` |
| `JF_PASSWORD` | 设备密码 | 否 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
| `JF_KEEPALIVE` | 保活时长(秒) | 否 | `300` |
---
## 🛠️ 功能
1. **获取设备 Token** - 通过设备序列号获取 24 小时有效的访问令牌
2. **设备登录认证** - 使用设备用户名/密码完成登录,获取 SessionID
3. **查询设备状态** - 获取设备在线状态、休眠状态、认证状态、IP 信息等
4. **设备云抓图** - 抓取设备实时图片(辅码流),图片地址有效期 24 小时
5. **获取直播地址** - 获取设备直播流地址(HLS/RTMP/FLV/WebRTC 等),默认有效期 10 小时
---
## 📖 详细文档
### 1. 获取设备 Token
**接口**: `POST /gwp/v3/rtc/device/token`
**响应**:
```json
{
"code": 2000,
"data": [{
"sn": "YOUR_DEVICE_SN",
"token": "ZTA3NTRiODMzNHw0OGRlOGMxYzFjMjBhNGEzfHwx..."
}]
}
```
**注意**: Token 有效期 24 小时,可缓存复用。
---
### 2. 查询设备状态
**接口**: `POST /gwp/v3/rtc/device/status`
**状态判定表**:
| status | wakeUpStatus | wakeUpEnable | 设备状态 |
|--------|--------------|--------------|----------|
| online | 空 | 空 | 常电设备,在线 |
| online | 0 | 1 | 低功耗设备,已休眠 |
| online | 1 | 1 | 低功耗设备,已唤醒 |
| online | 2 | 1 | 低功耗设备,准备休眠中 |
| notfound | 空 | 空 | 设备不在线 |
---
### 3. 设备云抓图
**接口**: `POST /gwp/v3/rtc/device/capture/{deviceToken}`
**注意**:
- ⚠️ **按调用次数计费** - 详见官网定价
- ⚠️ **图片有效期 24 小时** - 过期自动清除,需及时下载
---
### 4. 获取直播地址
**接口**: `POST /gwp/v3/rtc/device/livestream/{deviceToken}`
**支持协议**:
| 协议 | 参数 | 适用场景 |
|------|------|----------|
| HLS | `hls-ts` | Web 浏览器、移动端(推荐) |
| FLV | `flv` | Web 播放器 |
| WebRTC | `webrtc` | 超低延迟(仅 H.264) |
| RTMP | `rtmp-flv` | 微信小程序 |
**注意**:
- ⚠️ **直播地址默认有效期 10 小时**
- ⚠️ **低功耗设备** - 获取后 3 秒内必须播放
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 调用 login 接口登录 |
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
FILE:README.md
# JF Open Capture Livestream 技能
JF 杰峰智能设备鉴权与状态查询 AgentSkills (Python)。
---
## 📋 必需凭据
**使用前必须设置以下环境变量:**
| 参数 | 环境变量 | 类型 | 说明 | 来源 |
|------|----------|------|------|------|
| `uuid` | `JF_UUID` | string | 开放平台用户唯一标识 | 杰峰开放平台 |
| `appKey` | `JF_APPKEY` | string | 开放平台应用 Key | 杰峰开放平台 |
| `appSecret` | `JF_APPSECRET` | string | 应用密钥 | 杰峰开放平台 |
| `moveCard` | `JF_MOVECARD` | int | 签名算法偏移量 (0-9) | 杰峰开放平台 |
| `deviceSn` | `JF_SN` | string | 设备序列号 | 设备标签 |
⚠️ **如果缺少以上凭据,此技能无法正常工作!**
---
## 🔒 安全说明
**仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 目录结构
```
jf-open-pro-capture-livestream/
├── SKILL.md # 技能文档
├── README.md # 使用说明
└── scripts/
├── jf_open_pro_capture_livestream.py # Python SDK
└── requirements.txt # Python 依赖
```
---
## 快速开始
### 1. 设置环境变量
```bash
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"
```
### 2. 安装依赖
```bash
pip install -r scripts/requirements.txt
```
### 3. 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址(HLS 协议)
python scripts/jf_open_pro_capture_livestream.py livestream
```
---
## 功能
- ✅ 获取设备 Token(24 小时有效)
- ✅ 设备登录认证
- ✅ 查询设备状态(在线/离线/休眠)
- ✅ 自动签名计算
- ✅ 设备云抓图(图片有效期 24 小时)
- ✅ 获取直播地址(有效期 10 小时)
---
## 可用命令
| 命令 | 说明 |
|------|------|
| `status` | 查询设备状态 |
| `login` | 设备登录认证 |
| `capture` | 设备云抓图 |
| `livestream` | 获取直播地址 |
| `token` | 仅获取设备 Token |
---
## 依赖
- **Python:** 3.7+ (需要 `requests` 库)
---
## 文档
- `SKILL.md` - 完整技能文档
- `README.md` - 快速开始指南
FILE:scripts/jf_open_pro_capture_livestream.py
#!/usr/bin/env python3
"""
JF 杰峰设备认证与状态查询 Python SDK
功能:
- 获取设备 Token
- 设备登录
- 查询设备状态
- 设备云抓图
- 获取直播地址
用法:
# 设置环境变量
export JF_UUID="xxx" JF_APPKEY="xxx" JF_APPSECRET="xxx" JF_MOVECARD=5 JF_SN="xxx"
python jf_open_pro_capture_livestream.py status
安全说明:
✅ 仅支持环境变量,避免凭据泄露风险
🔒 不支持命令行参数或配置文件
"""
import hashlib
import os
import sys
import time
try:
import requests
except ImportError:
print("❌ 错误:需要安装 requests 库")
print("请运行:pip install -r requirements.txt")
sys.exit(1)
# ==================== 配置 ====================
DEFAULT_ENDPOINT = 'api.jftechws.com'
DEFAULT_MOVECARD = 5
def get_config():
"""
从环境变量获取配置
必需环境变量:
JF_UUID - 开放平台用户唯一标识
JF_APPKEY - 开放平台应用 Key
JF_APPSECRET - 开放平台应用密钥
JF_MOVECARD - 签名算法偏移量 (0-9)
JF_SN - 设备序列号
可选环境变量:
JF_USERNAME - 设备用户名 (默认 admin)
JF_PASSWORD - 设备密码
JF_ENDPOINT - API 端点 (默认 api.jftechws.com)
JF_KEEPALIVE - 保活时长 (默认 300)
"""
config = {
'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', DEFAULT_MOVECARD)),
'endpoint': os.environ.get('JF_ENDPOINT', DEFAULT_ENDPOINT),
'deviceSn': os.environ.get('JF_SN', ''),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', ''),
'keepaliveTime': int(os.environ.get('JF_KEEPALIVE', 300)),
}
return config
# ==================== JF 杰峰认证 SDK ====================
class JFAuth:
"""JF 杰峰设备认证 SDK"""
def __init__(self, uuid, app_key, app_secret, move_card, endpoint=DEFAULT_ENDPOINT):
"""
初始化 JF 认证
Args:
uuid: 开放平台用户 uuid(必需)
app_key: 开放平台应用 appKey(必需)
app_secret: 应用密钥(必需)
move_card: 签名算法参数(必需,int 类型 0-9)
endpoint: API 端点域名
"""
if not all([uuid, app_key, app_secret, move_card]):
raise ValueError("缺少必需的配置参数:uuid, app_key, app_secret, move_card")
self.uuid = uuid
self.app_key = app_key
self.app_secret = app_secret
self.endpoint = endpoint
self.move_card = move_card
def _str2byte(self, s):
"""字符串转字节数组(ISO-8859-1 编码)"""
return list(s.encode('iso-8859-1'))
def _change(self, encrypt_str, move_card):
"""简单移位算法"""
encrypt_byte = self._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(self, encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for i in range(length):
temp[i] = encrypt_byte[i]
temp[length * 2 - 1 - i] = change_byte[i]
return temp
def _get_signature(self, time_millis):
"""生成签名"""
encrypt_str = self.uuid + self.app_key + self.app_secret + time_millis
encrypt_byte = self._str2byte(encrypt_str)
change_byte = self._change(encrypt_str, self.move_card)
merged_byte = self._merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def _get_time_millis(self):
"""生成 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def _generate_request_id(self):
"""生成请求 ID"""
import uuid
return uuid.uuid4().hex
def _request(self, url, body, headers=None):
"""发送 HTTP 请求"""
if headers is None:
headers = {}
headers['Content-Type'] = 'application/json'
try:
response = requests.post(url, headers=headers, json=body, timeout=30)
return response.json()
except Exception as e:
return {'code': 0, 'msg': str(e)}
def get_device_token(self, device_sn):
"""
获取设备 Token(24 小时有效)
"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/token"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'sns': [device_sn], 'accessToken': ''}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
return {'success': True, 'token': result['data'][0]['token'], 'sn': result['data'][0]['sn']}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_login(self, device_token, username, password='', keepalive_time=300):
"""设备登录认证"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'UserName': username,
'PassWord': password,
'KeepaliveTime': keepalive_time
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {
'success': True,
'sessionId': result['data'].get('SessionID'),
'deviceType': result['data'].get('DeviceType'),
'aliveInterval': result['data'].get('AliveInterval'),
'channelNum': result['data'].get('ChannelNum')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def get_device_status(self, device_token):
"""查询设备状态"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/status"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'deviceTokenList': [device_token], 'region': 'Local'}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
device = result['data'][0]
status = device.get('status', 'unknown')
status_desc = '未知'
if status == 'online':
wake_status = device.get('wakeUpStatus')
if wake_status is None:
status_desc = '常电设备,在线'
elif wake_status == '0':
status_desc = '低功耗设备,已休眠'
elif wake_status == '1':
status_desc = '低功耗设备,已唤醒'
elif wake_status == '2':
status_desc = '低功耗设备,准备休眠中'
elif status == 'notfound':
status_desc = '设备不在线'
auth_status = device.get('authStatus')
auth_desc = '未知'
if auth_status is not None:
if auth_status == 1:
auth_desc = '认证成功'
elif auth_status == 0:
auth_desc = '正在认证'
elif auth_status == -1:
auth_desc = '认证未通过'
return {
'success': True,
'uuid': device.get('uuid'),
'status': status,
'statusDesc': status_desc,
'authStatus': auth_status,
'authDesc': auth_desc,
'wakeUpStatus': device.get('wakeUpStatus'),
'wakeUpEnable': device.get('wakeUpEnable'),
'wanIp': device.get('wanIp'),
'channel': device.get('channel')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_capture(self, device_token, channel=0, pic_type=0):
"""设备云抓图"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/capture/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'Name': 'OPSNAP',
'OPSNAP': {'Channel': channel, 'PicType': pic_type}
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'imageUrl': result['data'].get('image'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret')}
def get_live_stream(self, device_token, channel='0', stream='1', protocol='flv', username='admin', password='', expire_time=None):
"""获取直播地址"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/livestream/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'channel': channel,
'stream': stream,
'protocol': protocol,
'username': username,
'password': password
}
if expire_time:
body['expireTime'] = expire_time
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'url': result['data'].get('url'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret'), 'retMsg': result.get('data', {}).get('retMsg')}
# ==================== 输出函数 ====================
def print_device_status(status):
"""打印设备状态"""
print("\n=== 设备状态 ===")
print(f"设备序列号:{status['uuid']}")
print(f"状态:{status['status']} ({status['statusDesc']})")
if status.get('authDesc'):
print(f"认证状态:{status['authDesc']} ({status['authStatus']})")
if status.get('wakeUpStatus') is not None:
print(f"休眠状态:{status['wakeUpStatus']}")
if status.get('wakeUpEnable') is not None:
print(f"远程唤醒:{'支持' if status['wakeUpEnable'] == '1' else '不支持'}")
if status.get('wanIp'):
print(f"外网 IP: {status['wanIp']}")
# ==================== 主函数 ====================
def main():
if len(sys.argv) < 2:
print("用法:python jf_open_pro_capture_livestream.py <command>")
print("")
print("可用命令:")
print(" status 查询设备状态")
print(" login 设备登录")
print(" capture 设备云抓图")
print(" livestream 获取直播地址")
print(" token 仅获取设备 Token")
print("")
print("环境变量:")
print(" JF_UUID - 开放平台用户唯一标识(必需)")
print(" JF_APPKEY - 开放平台应用 Key(必需)")
print(" JF_APPSECRET - 开放平台应用密钥(必需)")
print(" JF_MOVECARD - 签名算法偏移量 (0-9)(必需)")
print(" JF_SN - 设备序列号(必需)")
print(" JF_USERNAME - 设备用户名(可选,默认 admin)")
print(" JF_PASSWORD - 设备密码(可选)")
print(" JF_ENDPOINT - API 端点(可选,默认 api.jftechws.com)")
sys.exit(1)
command = sys.argv[1]
# 从环境变量获取配置
config = get_config()
# 验证必需参数
if not config['uuid']:
print("❌ 错误:缺少必需环境变量 JF_UUID")
sys.exit(1)
if not config['appKey']:
print("❌ 错误:缺少必需环境变量 JF_APPKEY")
sys.exit(1)
if not config['appSecret']:
print("❌ 错误:缺少必需环境变量 JF_APPSECRET")
sys.exit(1)
if not config['deviceSn']:
print("❌ 错误:缺少必需环境变量 JF_SN")
sys.exit(1)
print("============================================================")
print("JF 杰峰设备认证工具 (Python)")
print("============================================================")
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{command}")
# 初始化 SDK
sdk = JFAuth(
uuid=config['uuid'],
app_key=config['appKey'],
app_secret=config['appSecret'],
move_card=config['moveCard'],
endpoint=config['endpoint']
)
device_token = None
try:
if command == 'token':
print("\n>>> 获取设备 Token...")
result = sdk.get_device_token(config['deviceSn'])
if result['success']:
print(f"✅ Token: {result['token']}")
else:
print(f"❌ 失败:{result['error']}")
elif command == 'status':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 查询设备状态...")
status_result = sdk.get_device_status(device_token)
if status_result['success']:
print_device_status(status_result)
else:
print(f"❌ 查询失败:{status_result['error']}")
elif command == 'login':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 设备登录...")
print(f"用户名:{config['userName']}, 保活时长:{config['keepaliveTime']}秒")
login_result = sdk.device_login(device_token, config['userName'], config['passWord'], config['keepaliveTime'])
if login_result['success']:
print("\n=== 登录成功 ===")
print(f"SessionID: {login_result['sessionId']}")
print(f"设备类型:{login_result['deviceType']}")
print(f"保活间隔:{login_result['aliveInterval']}秒")
print(f"通道数:{login_result['channelNum']}")
else:
print(f"❌ 登录失败:{login_result['error']}")
elif command == 'capture':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 执行云抓图...")
print("通道号:0, 图片类型:实时图片(辅码流)")
print("⚠️ 注意:云抓图按调用次数计费")
capture_result = sdk.device_capture(device_token, 0, 0)
if capture_result['success']:
print("\n=== 抓图成功 ===")
print(f"图片地址:{capture_result['imageUrl']}")
print("⚠️ 图片有效期 24 小时,请及时下载!")
print(f"\n下载命令:curl -o snapshot.png \"{capture_result['imageUrl']}\"")
else:
print(f"❌ 抓图失败:{capture_result['error']}")
if capture_result.get('ret'):
print(f"设备返回码:{capture_result['ret']}")
elif command == 'livestream':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 获取直播地址...")
print("通道号:0, 码流:标清(辅码流), 协议:flv")
print("⚠️ 注意:直播地址默认有效期 10 小时")
print("⚠️ 低功耗设备:获取后 3 秒内必须播放")
stream_result = sdk.get_live_stream(
device_token,
'0',
'1',
'flv',
config['userName'],
config['passWord']
)
if stream_result['success']:
print("\n=== 直播地址获取成功 ===")
print(f"播放地址:{stream_result['url']}")
print("\n使用方式:")
print(" - H5 播放:<video src=\"URL\" controls></video>")
print(" - VLC 播放:vlc \"URL\"")
print(" - ffmpeg: ffmpeg -i \"URL\" output.mp4")
print("\n⚠️ 地址有效期 10 小时,可重复使用")
else:
print(f"❌ 获取失败:{stream_result['error']}")
if stream_result.get('retMsg'):
print(f"设备信息:{stream_result['retMsg']}")
if stream_result.get('ret'):
print(f"设备返回码:{stream_result['ret']}")
else:
print(f"❌ 未知命令:{command}")
print("可用命令:status, login, capture, livestream, token")
except Exception as e:
print(f"❌ 执行出错:{e}")
import traceback
traceback.print_exc()
print("\n============================================================")
if __name__ == '__main__':
main()
FILE:scripts/requirements.txt
# JLink Python SDK 依赖
requests>=2.28.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
FILE:skill.yaml
# 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
- 云存回放
- 本地录像
FILE:_meta.json
{
"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"
]
}
FILE:scripts/cloud_playback_url.py
#!/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()
FILE:scripts/cloud_video_list.py
#!/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()
FILE:scripts/local_playback_url.py
#!/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'], password=config['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()
FILE:scripts/local_video_list.py
#!/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()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-video-record",
"installedVersion": "1.0.1",
"installedAt": 1775635200000
}
杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址,包括录像列表查询、回放地址获取、录像下载等功能。使用场景:云存回放、本地卡录像回放、录像下载、历史视频查看。
---
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
FILE:skill.yaml
# 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
- 云存回放
- 本地录像
FILE:_meta.json
{
"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"
]
}
FILE:scripts/cloud_playback_url.py
#!/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()
FILE:scripts/cloud_video_list.py
#!/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()
FILE:scripts/local_playback_url.py
#!/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'], password=config['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()
FILE:scripts/local_video_list.py
#!/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()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-video-record",
"installedVersion": "1.0.1",
"installedAt": 1775635200000
}
JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
---
name: jf-open-pro-ai-smart-search
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
# 必需凭证声明 - 平台元数据
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_USER
type: string
description: 用户 ID
default: admin
- 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 # Python 脚本
network_access:
- api.jftechws.com # 杰峰官方 API (国际)
- api-cn.jftech.com # 杰峰官方 API (中国大陆)
file_access: none # 不读取本地文件
---
# JF Open Pro AI Smart Search
> **面向开发者杰峰 AI 智搜工具 (Python)**
>
> 根据语义内容搜索杰峰云存报警视频,获取匹配的视频片段列表及播放信息。
---
## 🔒 安全说明
**凭据存储:仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 推荐方式,避免凭据出现在进程列表或日志中 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免明文存储凭据 |
**网络访问:**
- ✅ 仅访问杰峰官方 API 端点 (`api.jftechws.com` / `api-cn.jftech.com`)
- ❌ 不访问第三方服务
- ❌ 不读取本地文件系统
**脚本行为:**
- ✅ 本地执行 Python 脚本(技能本身)
- ✅ 仅向指定的杰峰 API 端点发起 HTTPS 请求
- ❌ 不执行外部命令
- ❌ 不读取敏感系统文件
---
## 🚀 快速开始
### 设置环境变量
```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_USER="admin" # 用户 ID(可选,默认:admin)
```
### 使用技能
```bash
# AI 智搜 - 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# AI 智搜 - 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# AI 智搜 - 搜索"狗"相关的视频
python scripts/search_video.py --search "狗"
# AI 智搜 - 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
# 获取云存回放地址(指定时间)
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性 | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USER` | 用户 ID | 否 | `admin` |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
---
## 🛠️ 功能
### 1. AI 智搜视频
根据语义内容搜索 AI 标记的云存报警视频。
**支持的搜索类型:**
| 搜索类型 | 示例查询 | 说明 |
|----------|----------|------|
| 人物 | "人"、"戴帽子的人"、"穿红色衣服的人" | 基于人形 + 属性检测 |
| 车辆 | "车"、"白色轿车"、"卡车" | 基于车辆检测 |
| 动物 | "狗"、"猫" | 基于动物检测 |
| 行为 | "跑步的人"、"摔倒" | 基于行为分析 |
**使用示例:**
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
```
**返回字段说明:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `st` | 录像开始时间(秒) | 1703275200 |
| `et` | 录像结束时间(秒) | 1703275260 |
| `matchRate` | 匹配度(0-1) | 0.95 |
| `queryTags` | 检测到的标签列表 | ["person", "hat"] |
| `eventTime` | 事件触发时间 | "2024-12-23 10:00:00" |
---
### 2. 云存回放地址获取
获取云存报警视频回放/播放地址。
**使用示例:**
```bash
# 指定时间范围获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
**工作流程:**
```
1. AI 智搜搜索视频
↓
获取云存报警信息视频列表
↓
2. 选择目标视频
↓
提取 st(开始时间)和 et(结束时间)
↓
3. 调用云存报警视频回放 API
↓
st 对应 startTime
et 对应 stopTime
↓
4. 获取播放链接
```
---
## 📖 使用场景示例
### 场景 1: 搜索特定人员的活动记录
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 查看返回结果,选择感兴趣的视频片段
# 使用返回的 st 和 et 获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
```
### 场景 2: 搜索车辆进出记录
```bash
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
```
### 场景 3: 完整流程 - 搜索并播放
```bash
# 一步完成:搜索"人"并获取第一个视频的回放地址
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `12504` | 授权失败 - 设备未开通 AI 智搜套餐 | 登录开放平台为设备绑定 AI 智搜套餐卡 |
| `10001` | 参数错误 | 检查请求参数格式 |
| `10002` | 签名失败 | 检查 appKey/appSecret 和时间戳 |
### 错误码 12504 处理
**错误信息:** `authorize failed, Please check it in the open platform`
**原因:** 设备未开通 AI 智搜服务,或未绑定套餐卡
**解决步骤:**
1. 登录杰峰开放平台:https://developer.jftech.com
2. 进入 **套餐管理** / **服务管理**
3. 找到 **AI 智搜** 或 **云存视频搜索** 套餐
4. 为设备购买并绑定套餐卡
5. 等待配置生效(通常 1-5 分钟)
6. 重新调用 API 测试
---
## ⚠️ 注意事项
1. **设备需开通云存服务** - AI 智搜需要云存套餐支持
2. **设备需开通 AI 智搜套餐** - 需在开放平台绑定套餐卡
3. **时间范围** - 只能搜索云存有效期内的视频
4. **搜索精度** - 受 AI 算法识别精度影响
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **AI 智搜文档**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=d2c0d9105d9c4b78bc0d2ee3851d2557
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
**可用脚本:**
| 脚本 | 功能 |
|------|------|
| `search_video.py` | AI 智搜 - 搜索云存报警视频 |
| `get_playback_url.py` | 获取云存回放地址(指定时间或完整流程) |
| `ai_search_playback.py` | 完整流程 - AI 智搜 + 播放地址一键获取 |
```bash
# 获取帮助
python scripts/search_video.py --help
python scripts/get_playback_url.py --help
python scripts/ai_search_playback.py --help
# AI 智搜
python scripts/search_video.py --search <搜索内容>
# 获取回放地址(指定时间)
python scripts/get_playback_url.py --start-time "YYYY-MM-DD HH:MM:SS" --stop-time "YYYY-MM-DD HH:MM:SS"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search <搜索内容> --video-index <索引>
```
脚本路径:`scripts/search_video.py`, `scripts/get_playback_url.py`, `scripts/ai_search_playback.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:skill.yaml
# JF Open Pro AI Smart Search - Skill Registry Metadata
# This file defines the skill's requirements for ClawHub registry
name: jf-open-pro-ai-smart-search
version: 1.0.0
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。
# 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/
- name: JF_SN
description: 设备序列号
source: 杰峰设备机身标签或管理后台
# Optional environment variables
optionalEnvVars:
- name: JF_USER
description: 用户 ID
default: admin
- 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: search_video.py
description: AI 智搜 - 搜索云存报警视频
entryPoint: scripts/search_video.py
- name: get_playback_url.py
description: 获取云存回放地址
entryPoint: scripts/get_playback_url.py
- name: ai_search_playback.py
description: 完整流程 - AI 智搜 + 播放地址
entryPoint: scripts/ai_search_playback.py
# Tags for discovery
tags:
- jf-tech
- 杰峰
- ai-search
- video-search
- cloud-storage
- 云存搜索
FILE:scripts/ai_search_playback.py
#!/usr/bin/env python3
"""
AI 智搜 + 云存回放完整流程脚本
工作流程:
1. 调用 AI 智搜 API 获取视频列表
2. 选择指定索引的视频
3. 提取开始/结束时间
4. 调用云存回放 API 获取播放地址
用法:
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"
export JF_USER="admin"
python ai_search_playback.py --search "人" --video-index 0
"""
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_jftech_sign(appkey: str, secret: str, timestamp: int) -> str:
"""生成 JF Tech API 签名"""
sign_str = f"{appkey}{timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str = "") -> dict:
"""调用 JF Tech AI 智搜 API"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""生成杰峰 API 签名"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
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 = {"deviceSnList": [sn]}
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 {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def get_playback_url(sn, user, start_time, stop_time, uuid, app_key, app_secret, movecard=0,
endpoint="api.jftechws.com", stream_type="hls"):
"""获取云存回放地址"""
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error') or token_result.get('code') != 2000:
return {"error": f"获取 Token 失败:{token_result.get('error') or token_result.get('msg')}"}
if not token_result.get('data') or len(token_result['data']) == 0:
return {"error": "获取 Token 失败:返回数据为空"}
device_token = token_result['data'][0]['token']
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"user": user,
"sn": sn,
"startTime": start_time,
"stopTime": stop_time,
"streamType": stream_type
}
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 {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": 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'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存回放完整流程',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 搜索"人"并获取第一个视频的回放地址
python ai_search_playback.py --search "人" --video-index 0
# 搜索"车"并获取第二个视频的回放地址
python ai_search_playback.py --search "车" --video-index 1
''')
parser.add_argument('--search', required=True, help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print('========================================')
print('AI 智搜 + 云存回放完整流程')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"搜索内容:{args.search}")
print(f"视频索引:{args.video_index}")
print()
# 步骤 1: AI 智搜
print('>>> 步骤 1/3: AI 智搜搜索视频...')
search_result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.search,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['appsecret']
)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
if search_result.get('message'):
print(f" 详情:{search_result['message']}")
sys.exit(1)
if search_result.get('code') != 2000:
print(f"❌ API 错误码:{search_result.get('code')}")
print(f" 详情:{search_result.get('msg', 'Unknown error')}")
sys.exit(1)
data = search_result.get('data', {})
videos = data.get('videos', [])
if not videos:
print('❌ 未找到匹配的视频')
sys.exit(1)
print(f"✅ 找到 {len(videos)} 个匹配的视频")
if args.video_index >= len(videos):
print(f"❌ 视频索引 {args.video_index} 超出范围 (0-{len(videos)-1})")
sys.exit(1)
video = videos[args.video_index]
print(f" 选择:片段 {args.video_index + 1}")
print(f" 时间:{video.get('eventTime', 'N/A')}")
print(f" 匹配度:{video.get('matchRate', 0):.0%}")
print()
# 步骤 2: 提取时间
start_time = video.get('st')
stop_time = video.get('et')
if not start_time or not stop_time:
print('❌ 无法提取视频时间信息')
sys.exit(1)
# 转换时间戳为可读格式
from datetime import datetime
start_dt = datetime.fromtimestamp(start_time)
stop_dt = datetime.fromtimestamp(stop_time)
print('>>> 步骤 2/3: 提取视频时间...')
print(f" 开始:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} ({start_time})")
print(f" 结束:{stop_dt.strftime('%Y-%m-%d %H:%M:%S')} ({stop_time})")
print()
# 步骤 3: 获取回放地址
print('>>> 步骤 3/3: 获取云存回放地址...')
playback_result = get_playback_url(
sn=config['sn'],
user=config['user'],
start_time=start_time,
stop_time=stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error'):
print(f"❌ 获取回放地址失败:{playback_result['error']}")
sys.exit(1)
if playback_result.get('code') != 2000:
print(f"❌ API 错误码:{playback_result.get('code')}")
print(f" 详情:{playback_result.get('msg', 'Unknown error')}")
sys.exit(1)
playback_data = playback_result.get('data', {})
playback_url = playback_data.get('url') or playback_data.get('playUrl')
if not playback_url:
print('❌ 未找到播放 URL')
print(json.dumps(playback_result, indent=2, ensure_ascii=False))
sys.exit(1)
print('✅ 回放地址获取成功')
print()
print('========================================')
print('播放信息')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"时间范围:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} - {stop_dt.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"播放地址:{playback_url}")
print()
print("使用方式:")
print(f" - VLC 播放:vlc \"{playback_url}\"")
print(f" - 网页播放:在浏览器中打开 URL")
print(f" - 下载:curl -o video.mp4 \"{playback_url}\"")
print('========================================')
if __name__ == "__main__":
main()
FILE:scripts/get_playback_url.py
#!/usr/bin/env python3
"""
云存报警视频回放地址获取脚本
工作流程:
1. 先通过 AI 智搜获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
API 端点:
1. 获取设备 Token: POST https://api.jftechws.com/gwp/v3/rtc/device/token
2. 云存回放:POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
用法:
# 设置环境变量(必需)
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn"
export JF_USER="admin" # 可选,默认 admin
export JF_ENDPOINT="api.jftechws.com" # 可选
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-03-28 15:23:26" --stop-time "2026-03-28 15:23:36"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""
生成杰峰 API 签名
签名算法:MD5(uuid + appKey + timeMillis + secret)
Args:
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
time_millis: 时间戳(毫秒),已包含 movecard 偏移
"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
通过设备序列号生成 deviceToken
API: POST https://api.jftechws.com/gwp/v3/rtc/device/token
Args:
sn: 设备序列号
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应,包含 deviceToken
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
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 = {"deviceSnList": [sn]}
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_playback_url(device_token, sn, user, start_time, stop_time,
uuid, app_key, app_secret, movecard=0,
channel=0, stream_type=1, endpoint="api.jftechws.com"):
"""
获取云存报警视频回放或下载地址
根据录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
获取对应云存报警视频播放链接
API: POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
Args:
device_token: 设备 Token(从 get_device_token 获取)
sn: 设备序列号
user: 用户 ID
start_time: 录像开始时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 st 字段)
stop_time: 录像结束时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 et 字段)
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
channel: 通道号(默认 0)
stream_type: 码流类型(1=辅码流,2=主码流,默认 1)
endpoint: API 端点
Returns:
dict: API 响应,包含播放地址
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
# 云存回放 API 端点
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"
}
# 请求体:startTime 对应 st,stopTime 对应 et
body = {
"sn": sn,
"user": user,
"startTime": start_time,
"stopTime": stop_time,
"channel": channel,
"streamType": stream_type
}
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 ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
AI 智搜 - 搜索云存报警视频
API: POST https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
url = f"https://{endpoint}/aisvr/v3/gateway/api/viewsearch/searchVideo"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"sn": sn,
"user": user,
"searchContent": search_content
}
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}"}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def ai_search_and_playback(sn, user, search_content, uuid, app_key, app_secret, movecard=0,
video_index=0, endpoint="api.jftechws.com"):
"""
完整流程:AI 智搜 + 云存报警视频回放地址获取
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
video_index: 选择第几个视频(从 0 开始)
endpoint: API 端点
Returns:
dict: 包含搜索结果和播放地址
"""
print("=" * 70)
print("🎬 AI 智搜 + 云存报警视频回放地址获取")
print("=" * 70)
print()
print(f"设备 SN: {sn}")
print(f"用户:{user}")
print(f"搜索内容:{search_content}")
print(f"选择视频索引:{video_index}")
print()
# 步骤 1: AI 智搜 - 获取云存报警信息视频列表
print(">>> 步骤 1: 搜索视频获取云存报警信息视频列表...")
search_result = ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard, endpoint)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
return {"error": search_result['error']}
if search_result.get('code') != 2000:
print(f"❌ AI 智搜失败:{search_result.get('msg', 'Unknown error')}")
return {"error": search_result.get('msg', 'Unknown error')}
videos = search_result.get('data', [])
if not videos:
print("❌ 未找到匹配的视频")
return {"error": "No videos found"}
print(f"✅ AI 智搜成功,找到 {len(videos)} 条视频")
print()
# 选择指定索引的视频
if video_index >= len(videos):
print(f"❌ 视频索引 {video_index} 超出范围(0-{len(videos)-1})")
return {"error": f"Video index {video_index} out of range"}
video = videos[video_index]
print(f"📹 选择第 {video_index + 1} 个视频:")
print(f" 录像开始时间(st):{video['st']}")
print(f" 录像结束时间(et):{video['et']}")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
# 步骤 2: 通过设备序列号生成 deviceToken
print(">>> 步骤 2: 通过设备序列号生成 deviceToken...")
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error'):
print(f"❌ 获取 deviceToken 失败:{token_result['error']}")
return {"error": token_result['error'], "search_result": search_result}
if token_result.get('code') != 2000:
print(f"❌ 获取 deviceToken 失败:{token_result.get('msg', 'Unknown error')}")
return {"error": token_result.get('msg', 'Unknown error'), "search_result": search_result}
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功:{device_token[:30]}...")
print()
# 步骤 3: 获取云存报警视频回放地址
print(">>> 步骤 3: 获取云存报警视频回放地址...")
print(f" API 端点:POST /gwp/v3/rtc/device/getVideoUrl/{device_token[:30]}...")
print(f" startTime: {video['st']} (对应 st)")
print(f" stopTime: {video['et']} (对应 et)")
print()
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=sn,
user=user,
start_time=video['st'], # st 对应 startTime
stop_time=video['et'], # et 对应 stopTime
uuid=uuid,
app_key=app_key,
app_secret=app_secret,
endpoint=endpoint
)
if playback_result.get('error'):
print(f"❌ 获取播放地址失败:{playback_result['error']}")
return {"error": playback_result['error'], "search_result": search_result}
if playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('msg', 'Unknown error')}")
return {"error": playback_result.get('msg', 'Unknown error'), "search_result": search_result}
# 成功获取播放地址
play_url = playback_result['data'].get('url')
print("✅ 云存报警视频播放地址获取成功!")
print()
print("=" * 70)
print("🎬 播放地址")
print("=" * 70)
print()
print(f"📹 视频信息:")
print(f" 时间:{video['st']} - {video['et']}")
print(f" 时长:10 秒")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
print(f"🔗 播放地址:")
print(f" {play_url}")
print()
print("=" * 70)
print("🎯 播放方式:")
print("=" * 70)
print()
print("1. VLC 播放器:")
print(f' vlc "{play_url}"')
print()
print("2. 网页播放(HLS.js):")
print(f' <video src="{play_url}" controls></video>')
print()
print("3. FFmpeg 下载:")
print(f' ffmpeg -i "{play_url}" -c copy video.mp4')
print()
return {
"success": True,
"search_result": search_result,
"playback_result": playback_result,
"video_info": video,
"play_url": play_url
}
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'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存报警视频回放地址获取',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
使用流程:
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
'''
)
parser.add_argument('--search', help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
parser.add_argument('--start-time', help='录像开始时间(格式:YYYY-MM-DD HH:MM:SS)')
parser.add_argument('--stop-time', help='录像结束时间(格式:YYYY-MM-DD HH:MM:SS)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
# 如果有 search 参数,执行完整流程
if args.search:
result = ai_search_and_playback(
sn=config['sn'],
user=config['user'],
search_content=args.search,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
video_index=args.video_index,
endpoint=config['endpoint']
)
# 如果有 start_time 和 stop_time 参数,直接获取播放地址
elif args.start_time and args.stop_time:
print(">>> 通过设备序列号生成 deviceToken...")
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"❌ 获取 deviceToken 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功")
print(">>> 获取云存报警视频回放地址...")
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=config['sn'],
user=config['user'],
start_time=args.start_time,
stop_time=args.stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error') or playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('error') or playback_result.get('msg')}")
sys.exit(1)
play_url = playback_result['data'].get('url')
print(f"✅ 播放地址:{play_url}")
result = {"success": True, "play_url": play_url}
else:
parser.print_help()
sys.exit(1)
# 输出 JSON 结果
print()
print("=" * 70)
print("📋 JSON 结果")
print("=" * 70)
print()
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result.get('success') else 1)
if __name__ == '__main__':
main()
FILE:scripts/search_video.py
#!/usr/bin/env python3
"""
AI 智搜脚本 - 搜索云存报警视频
仅支持环境变量配置凭据,避免命令行泄露风险。
支持平台:JF Tech(杰峰)
用法:
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"
export JF_USER="admin"
python search_video.py --search "人"
python search_video.py --search "车"
python search_video.py --search "戴帽子的人"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_jftech_sign(appkey: str, secret: str, timestamp: int, movecard: int = 0) -> str:
"""
生成 JF Tech API 签名
Args:
appkey: 应用 appKey
secret: 应用密钥
timestamp: 时间戳(毫秒)
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
# 时间戳加上 movecard 偏移量
adjusted_timestamp = timestamp + movecard
sign_str = f"{appkey}{adjusted_timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str, movecard: int = 0) -> dict:
"""
调用 JF Tech AI 智搜 API
Args:
sn: 设备序列号
user: 用户 ID
query: 搜索内容(语义描述)
uuid: 开放平台用户 uuid
appkey: 应用 appKey
secret: 应用密钥
authorization: 用户 token
movecard: 签名算法偏移量 (0-9)
Returns:
API 响应字典
"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp, movecard)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def format_results(results: dict) -> str:
"""格式化搜索结果输出"""
if "error" in results:
return f"❌ 错误:{results.get('error', 'Unknown')}\n{results.get('message', '')}"
if results.get("code") != 2000:
return f"❌ API 错误码:{results.get('code')}\n{results.get('msg', '')}"
data = results.get("data", {})
videos = data.get("videos", [])
if not videos:
return "📭 未找到匹配的视频"
output = []
output.append(f"✅ 找到 {len(videos)} 个匹配的视频片段\n")
for i, video in enumerate(videos, 1):
output.append(f"📹 片段 {i}:")
output.append(f" 时间:{video.get('eventTime', 'N/A')}")
output.append(f" 匹配度:{video.get('matchRate', 0):.0%}")
output.append(f" 标签:{', '.join(video.get('queryTags', []))}")
output.append(f" 大小:{video.get('vidsz', 0) / 1024:.1f} KB")
if video.get('picfg') == 1:
output.append(f" 缩略图:有")
output.append("")
return "\n".join(output)
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'),
'secret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description="AI 智搜 - 搜索云存报警视频",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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"
export JF_USER="admin"
# 搜索"人"相关的视频
python search_video.py --search "人"
# 搜索"车"相关的视频
python search_video.py --search "车"
# 搜索"戴帽子的人"
python search_video.py --search "戴帽子的人"
''')
parser.add_argument("--search", dest="query", required=True, 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)
result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.query,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['secret'],
authorization='', # 如需 authorization 可从环境变量添加
movecard=config['movecard']
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_results(result))
if __name__ == "__main__":
main()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-ai-smart-search",
"installedVersion": "1.0.3",
"installedAt": 1775547600000
}
面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
---
name: jf-open-pro-ptz-control
description: 面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
# 必需凭证声明 - 平台元数据
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 # 仅支持 Python
---
# JF Open Pro PTZ Control
> **面向开发者杰峰设备云台控制工具 (Python)**
>
> 支持设备状态查询、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```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" # 设备密码(可选)
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽(开启)
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 一键遮蔽(关闭)
python scripts/jf_open_pro_ptz_control.py mask --enable false
# 变倍控制(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
# 添加巡航点
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 获取巡航列表
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `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. 设备状态查询
查询设备在线状态、休眠状态、认证状态、设备 WAN IP 等。
```bash
python scripts/jf_open_pro_ptz_control.py status
```
**返回信息:**
- 设备在线状态(online/notfound)
- 低功耗设备休眠状态
- 认证状态
- 设备 WAN IP
---
### 2. 方向控制 (PTZ)
云台支持 8 个方向转动:
| 方向 | 参数值 |
|------|--------|
| 上 | `up` |
| 下 | `down` |
| 左 | `left` |
| 右 | `right` |
| 左上 | `leftup` |
| 左下 | `leftdown` |
| 右上 | `rightup` |
| 右下 | `rightdown` |
**使用示例:**
```bash
# 开始向上转动(速度 5)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start --step 5
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
```
**参数说明:**
- `--direction`: 方向(up/down/left/right/leftup/leftdown/rightup/rightdown)
- `--action`: 动作(start/stop)
- `--step`: 速度(1-8,1 最慢,8 最快,默认:5)
⚠️ **重要**: 必须先发送 start 再发送 stop,建议间隔 500ms。如果不发送 stop,设备会一直转动到最大角度。
---
### 3. 一键遮蔽 (Mask)
开启后摄像头转至最下方然后转至最右侧,同时关闭视频预览和录像。
```bash
# 开启遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 关闭遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable false
```
---
### 4. 变倍和聚焦控制 (Zoom/Focus)
支持变倍(Zoom)和聚焦(Focus)操作:
| 功能 | 参数值 | 说明 |
|------|--------|------|
| 变倍 - | `ZoomWide` | 缩小(广角) |
| 变倍 + | `ZoomTile` | 放大(长焦) |
| 聚焦 - | `FocusFar` | 聚焦远处 |
| 聚焦 + | `FocusNear` | 聚焦近处 |
| 光圈 - | `IrisSmall` | 缩小光圈 |
| 光圈 + | `IrisLarge` | 放大光圈 |
**使用示例:**
```bash
# 开始变倍 +(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start --step 8
# 停止
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
```
---
### 5. 预置位管理 (Preset)
预置点编号范围:1-255(建议不使用 200 以后的编号)
**特殊预置点:**
- `100`: 移动追踪守望位(追踪停止后自动回归)
- `128`: 自检回归预置点(设备重启或自检时回归)
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 设置预置点 | `set` | 将当前位置保存为预置点 |
| 删除预置点 | `clear` | 删除指定预置点 |
| 转到预置点 | `goto` | 云台转动到预置点位置 |
| 编辑预置点名 | `name` | 修改预置点名称 |
| 获取列表 | `list` | 获取所有预置点 |
**使用示例:**
```bash
# 设置预置点 1,名称为"门口"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 删除预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command clear --id 1
# 编辑预置点名称
python scripts/jf_open_pro_ptz_control.py preset --preset-command name --id 1 --name "新名称"
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
```
---
### 6. 巡航计划管理 (Tour)
巡航功能让设备在多个预置点之间自动循环巡视。
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 添加巡航点 | `add` | 往巡航线路添加预置点 |
| 删除巡航点 | `delete` | 从巡航线路删除预置点 |
| 启动巡航 | `start` | 开始自动巡航 |
| 停止巡航 | `stop` | 停止巡航 |
| 清除巡航线路 | `clear` | 清空整个巡航线路 |
| 获取列表 | `list` | 获取巡航配置 |
**使用示例:**
```bash
# 添加预置点 1 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1 --step 5
# 添加预置点 2 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
# 启动巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 停止巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command stop --tour-id 0
# 获取巡航配置
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📖 使用场景示例
### 场景 1: 基础云台控制
```bash
# 1. 检查设备状态
python scripts/jf_open_pro_ptz_control.py status
# 2. 向上转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 3. 向右转动
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action stop
```
### 场景 2: 设置并使用预置位
```bash
# 1. 转动到目标位置
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 2. 保存为预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 3. 转动到其他位置...
# 4. 回到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
```
### 场景 3: 设置自动巡航
```bash
# 1. 设置多个预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "位置 1"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 2 --name "位置 2"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 3 --name "位置 3"
# 2. 添加到巡航线路
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 3
# 3. 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 脚本会自动处理登录 |
| `526` | 低电量/不支持 | 设备电量不足或为固定摄像头 |
### 错误码 526 说明
**含义:** 设备支持云台,但电量过低无法执行
**解决方案:**
1. 给设备充电
2. 等待电量恢复至 20% 以上
3. 使用电源供电模式
---
## ⚠️ 注意事项
1. **设备需在线** - 操作前确保设备在线
2. **设备需登录** - 脚本会自动处理设备登录
3. **PTZ 控制** - start/stop 指令需串行发送(间隔 500ms)
4. **预置点范围** - 建议使用 1-199 编号
5. **电量检查** - 低电量时云台功能可能被禁用
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
```bash
# 获取帮助
python scripts/jf_open_pro_ptz_control.py --help
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# PTZ 方向控制
python scripts/jf_open_pro_ptz_control.py ptz --direction <方向> --action <start|stop>
# 一键遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable <true|false>
# 变倍聚焦
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command <命令> --action <start|stop>
# 预置点管理
python scripts/jf_open_pro_ptz_control.py preset --preset-command <set|clear|goto|name|list> [选项]
# 巡航管理
python scripts/jf_open_pro_ptz_control.py tour --tour-command <add|delete|start|stop|clear|list> [选项]
```
脚本路径:`scripts/jf_open_pro_ptz_control.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:scripts/jf_open_pro_ptz_control.py
#!/usr/bin/env python3
"""
JF 杰峰云台 PTZ 控制工具 - Python 版本
仅支持环境变量配置凭据,避免命令行泄露风险。
用法:
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"
export JF_USERNAME="admin"
export JF_PASSWORD="your-password"
python jf_open_pro_ptz_control.py status
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py mask --enable true
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
"""
import argparse
import hashlib
import json
import os
import time
import urllib.request
import urllib.error
# ==================== 工具函数 ====================
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 get_signature(uuid, app_key, app_secret, time_millis, move_card=5):
"""获取签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, move_card)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_time_millis():
"""获取 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def generate_request_id():
"""生成 32 位请求 ID"""
import random
return ''.join(random.choice('0123456789abcdef') for _ in range(32))
# ==================== HTTP 请求 ====================
def https_post(url, data, headers):
"""发送 HTTPS POST 请求"""
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
headers=headers,
method='POST'
)
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
raise Exception(f'HTTP 错误:{e.code} - {e.reason}')
except urllib.error.URLError as e:
raise Exception(f'请求失败:{e.reason}')
# ==================== 认证相关 ====================
def get_device_token(config):
"""获取设备 Token"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/token"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'sns': [config['deviceSn']], 'accessToken': ''}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取 Token 失败:{response.get('msg')} (code: {response.get('code')})")
if not response.get('data') or len(response['data']) == 0:
raise Exception('获取 Token 失败:返回数据为空')
return response['data'][0]['token']
def device_login(config, device_token, keepalive_time=300):
"""设备登录"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'UserName': config['userName'],
'PassWord': config['passWord'] or '',
'KeepaliveTime': keepalive_time
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设备登录失败:{response.get('msg')} (code: {response.get('code')})")
if response.get('data', {}).get('Ret') != 100:
raise Exception(f"设备登录失败:设备返回码 {response['data']['Ret']}")
return response['data']
# ==================== 设备状态查询 ====================
def get_device_status(config):
"""获取设备状态"""
print('>>> 获取设备 Token...')
device_token = get_device_token(config)
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/status"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'deviceTokenList': [device_token], 'region': 'Local'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"查询状态失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'][0]
# ==================== PTZ 方向控制 ====================
DIRECTION_MAP = {
'up': 'DirectionUp',
'down': 'DirectionDown',
'left': 'DirectionLeft',
'right': 'DirectionRight',
'leftup': 'DirectionLeftUp',
'leftdown': 'DirectionLeftDown',
'rightup': 'DirectionRightUp',
'rightdown': 'DirectionRightDown'
}
def ptz_control(config, device_token, direction, action, step=5, channel=0):
"""云台方向控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command = DIRECTION_MAP.get(direction.lower())
if not command:
raise Exception(f'无效的方向:{direction}')
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Preset': preset,
'Channel': channel,
'Step': step
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"PTZ 控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 一键遮蔽 ====================
def set_mask(config, device_token, enable):
"""一键遮蔽"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'Name': 'General.OneKeyMaskVideo',
'General.OneKeyMaskVideo': [{'Enable': enable}]
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"一键遮蔽设置失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 变倍聚焦控制 ====================
def zoom_focus_control(config, device_token, command, action, step=8, channel=0):
"""变倍聚焦控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Channel': channel,
'Step': step,
'Preset': preset
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"变倍聚焦控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 预置点管理 ====================
def preset_control(config, device_token, command, preset_id, preset_name='', channel=0):
"""预置点控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'set': 'SetPreset',
'clear': 'ClearPreset',
'goto': 'GotoPreset',
'name': 'SetPresetName'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的预置点命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Preset': preset_id,
'Channel': channel
}
}
}
if command in ['set', 'name']:
data['OPPTZControl']['Parameter']['PresetName'] = preset_name or f'预置点{preset_id}'
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"预置点操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_preset_list(config, device_token):
"""获取预置点列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZPreset'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取预置点列表失败:{response.get('msg')} (code: {response.get('code')})")
# API 返回的是二维数组,需要扁平化
presets = response['data'].get('Uart.PTZPreset', [])
return [item for sublist in presets for item in sublist] if presets else []
# ==================== 巡航管理 ====================
def tour_control(config, device_token, command, tour_id=0, preset_id=0, step=5, channel=0):
"""巡航控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'add': 'AddTour',
'delete': 'DeleteTour',
'start': 'StartTour',
'stop': 'StopTour',
'clear': 'ClearTour'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的巡航命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Tour': tour_id,
'Channel': channel
}
}
}
if command in ['add', 'delete']:
data['OPPTZControl']['Parameter']['Preset'] = preset_id
data['OPPTZControl']['Parameter']['Step'] = step
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"巡航操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_tour_list(config, device_token):
"""获取巡航列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZTour'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取巡航列表失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'].get('Uart.PTZTour', [])
# ==================== 命令行参数解析 ====================
def parse_args():
parser = argparse.ArgumentParser(
description='JLink 杰峰云台 PTZ 控制工具 - Python 版本',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USERNAME 设备用户名,默认 admin (可选)
JF_PASSWORD 设备密码 (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
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 jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽
python jf_open_pro_ptz_control.py mask --enable true
# 变倍控制(放大)
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 添加巡航点
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
''')
parser.add_argument('command', choices=['status', 'ptz', 'mask', 'zoom', 'preset', 'tour'], help='命令')
# PTZ 参数
parser.add_argument('--direction', choices=['up', 'down', 'left', 'right', 'leftup', 'leftdown', 'rightup', 'rightdown'], help='方向')
parser.add_argument('--action', choices=['start', 'stop'], help='动作')
parser.add_argument('--step', type=int, default=5, help='速度 1-8(默认:5)')
# Mask 参数
parser.add_argument('--enable', type=str, help='开启/关闭遮蔽 (true/false)')
# Zoom 参数
parser.add_argument('--zoom-command', dest='zoom_command', help='变倍/聚焦命令')
# Preset 参数
parser.add_argument('--preset-command', dest='preset_command', help='预置点操作 (set/clear/goto/name/list)')
parser.add_argument('--id', type=int, help='预置点 ID')
parser.add_argument('--name', help='预置点名称')
# Tour 参数
parser.add_argument('--tour-command', dest='tour_command', help='巡航操作 (add/delete/start/stop/clear/list)')
parser.add_argument('--tour-id', dest='tour_id', type=int, default=0, help='巡航线路 ID(默认:0)')
parser.add_argument('--preset-id', dest='preset_id', type=int, help='预置点 ID')
return parser.parse_args()
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)),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com'),
'deviceSn': os.environ.get('JF_SN'),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', '')
}
def main():
args = parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
return 1
try:
if args.command == 'status':
print('========================================')
print('JLink 设备状态查询')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print()
status = get_device_status(config)
print('=== 设备状态 ===')
print(f"设备序列号:{status.get('uuid')}")
print(f"状态:{status.get('status')}")
print(f"认证状态:{status.get('authStatus')}")
if status.get('wakeUpStatus') is not None:
print(f"唤醒状态:{status.get('wakeUpStatus')}")
if status.get('wakeUpEnable') is not None:
print(f"支持唤醒:{status.get('wakeUpEnable')}")
if status.get('wanIp'):
print(f"WAN IP: {status.get('wanIp')}")
elif args.command == 'ptz':
if not args.direction or not args.action:
print('❌ PTZ 命令需要 --direction 和 --action 参数')
return 1
print('========================================')
print('JLink 云台方向控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"方向:{args.direction}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = ptz_control(config, device_token, args.direction, args.action, args.step)
print('✅ PTZ 控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'mask':
if args.enable is None:
print('❌ Mask 命令需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
print('========================================')
print('JLink 一键遮蔽')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"开启遮蔽:{enable}")
print()
device_token = get_device_token(config)
result = set_mask(config, device_token, enable)
print(f"✅ 一键遮蔽{'开启' if enable else '关闭'}成功")
print(f"返回码:{result.get('Ret')}")
elif args.command == 'zoom':
if not args.zoom_command or not args.action:
print('❌ Zoom 命令需要 --zoom-command 和 --action 参数')
return 1
print('========================================')
print('JLink 变倍聚焦控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{args.zoom_command}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = zoom_focus_control(config, device_token, args.zoom_command, args.action, args.step)
print('✅ 变倍聚焦控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'preset':
if not args.preset_command:
print('❌ Preset 命令需要 --preset-command 参数')
return 1
print('========================================')
print('JLink 预置点管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.preset_command}")
if args.id is not None:
print(f"预置点 ID: {args.id}")
if args.name:
print(f"名称:{args.name}")
print()
device_token = get_device_token(config)
if args.preset_command == 'list':
presets = get_preset_list(config, device_token)
print('=== 预置点列表 ===')
if not presets:
print('暂无预置点')
else:
for p in presets:
print(f" ID {p.get('Id')}: {p.get('PresetName')}")
else:
if args.id is None:
print('❌ 需要指定 --id 参数')
return 1
result = preset_control(config, device_token, args.preset_command, args.id, args.name or '')
print(f'✅ 预置点{args.preset_command}成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'tour':
if not args.tour_command:
print('❌ Tour 命令需要 --tour-command 参数')
return 1
print('========================================')
print('JLink 巡航管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.tour_command}")
print(f"巡航线路 ID: {args.tour_id}")
if args.preset_id is not None:
print(f"预置点 ID: {args.preset_id}")
print()
device_token = get_device_token(config)
if args.tour_command == 'list':
tours = get_tour_list(config, device_token)
print('=== 巡航线路列表 ===')
print(json.dumps(tours, indent=2, ensure_ascii=False))
else:
result = tour_control(config, device_token, args.tour_command, args.tour_id, args.preset_id or 0, args.step)
print(f'✅ 巡航{args.tour_command}成功')
print(f"返回码:{result.get('Ret')}")
print('========================================')
return 0
except Exception as e:
print(f'❌ 错误:{e}')
return 1
if __name__ == '__main__':
exit(main())
面向开发者控制杰峰设备相关配置,支持动检检测开关和灵敏度设置、人形检测开关和检测灵敏度、人形追踪开关和追踪灵敏度等。触发词:摄像机配置、动检设置、人形检测、人形追踪、灵敏度配置。
---
name: jf-open-pro-camera-config
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_ENDPOINT
type: string
description: API 端点
default: api.jftechws.com
- name: JF_CHANNEL
type: integer
description: 通道号
default: 0
# 网络端点声明
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 # 仅支持 Python
---
# JF Open Pro Camera Config
> **面向开发者杰峰设备配置工具 (Python)**
>
> 支持动检检测开关和灵敏度设置、人形检测开关和检测灵敏度、人形追踪开关和追踪灵敏度等。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```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_CHANNEL=0 # 通道号(可选,默认:0)
```
### 使用技能
```bash
# 移动侦测 - 获取配置
python scripts/jf_open_pro_camera_config.py motion-detect --action get
# 移动侦测 - 开启(布防),灵敏度 3
python scripts/jf_open_pro_camera_config.py motion-detect --action set --enable true --level 3
# 移动侦测 - 关闭(撤防)
python scripts/jf_open_pro_camera_config.py motion-detect --action set --enable false
# 人形检测 - 获取配置
python scripts/jf_open_pro_camera_config.py human-detection --action get
# 人形检测 - 开启,灵敏度 2
python scripts/jf_open_pro_camera_config.py human-detection --action set --enable true --sensitivity 2
# 人形检测 - 关闭
python scripts/jf_open_pro_camera_config.py human-detection --action set --enable false
# 人形追踪 - 获取配置
python scripts/jf_open_pro_camera_config.py human-track --action get
# 人形追踪 - 开启,灵敏度 1,10 秒回位
python scripts/jf_open_pro_camera_config.py human-track --action set --enable true --sensitivity 1 --return-time 10
# 人形追踪 - 关闭
python scripts/jf_open_pro_camera_config.py human-track --action set --enable false
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
| `JF_CHANNEL` | 通道号 | 否 | `0` |
---
## 🛠️ 功能
### 1. 移动侦测配置 (Motion Detect)
用于检测画面中的物体移动,触发报警。
| 参数 | 取值范围 | 说明 |
|------|---------|------|
| Enable | true/false | 布防/撤防开关 |
| Level | 1-6 | 灵敏度,1 最低,6 最高 |
**灵敏度说明:**
| 值 | 说明 |
|----|------|
| 1 | 最低灵敏度 |
| 2 | 较低灵敏度 |
| 3 | 中等灵敏度 |
| 4 | 较高灵敏度 |
| 5 | 很高灵敏度 |
| 6 | 最高灵敏度 |
---
### 2. 人形检测配置 (Human Detection)
使用 AI 算法检测画面中的人形目标,比移动侦测更准确。
| 参数 | 取值范围 | 说明 |
|------|---------|------|
| Enable | true/false | 开关 |
| Sensitivity | 0-3 | 灵敏度,0 低,1 中,2 高 |
| ObjectType | 0-1 | 0 检测人,1 检测物体 |
**灵敏度说明:**
| 值 | 说明 |
|----|------|
| 0 | 低灵敏度 |
| 1 | 中灵敏度 |
| 2 | 高灵敏度 |
| 3 | 灵敏度数量(只读) |
**检测类型:**
| 值 | 说明 |
|----|------|
| 0 | 检测人 |
| 1 | 检测物体 |
---
### 3. 人形追踪配置 (Human Track)
云台自动跟随人形目标移动。
| 参数 | 取值范围 | 说明 |
|------|---------|------|
| Enable | 0/1 | 开关(整数而非布尔值) |
| Sensitivity | 0-2 | 灵敏度,0 低,1 中,2 高 |
| ReturnTime | 0-600 | 无人后回位时间(秒),0 不返回 |
**灵敏度说明:**
| 值 | 说明 |
|----|------|
| 0 | 低灵敏度 |
| 1 | 中灵敏度 |
| 2 | 高灵敏度 |
**回位时间:**
| 值 | 说明 |
|----|------|
| 0 | 不返回 |
| 1-600 | 秒后返回默认位置(守望位) |
---
## 📖 使用场景示例
### 场景 1: 开启所有检测功能
```bash
# 开启移动侦测(灵敏度 3)
python scripts/jf_open_pro_camera_config.py motion-detect --action set --enable true --level 3
# 开启人形检测(灵敏度 2)
python scripts/jf_open_pro_camera_config.py human-detection --action set --enable true --sensitivity 2
# 开启人形追踪(灵敏度 1,10 秒回位)
python scripts/jf_open_pro_camera_config.py human-track --action set --enable true --sensitivity 1 --return-time 10
```
### 场景 2: 夜间模式(只保留移动侦测)
```bash
# 关闭人形检测
python scripts/jf_open_pro_camera_config.py human-detection --action set --enable false
# 关闭人形追踪
python scripts/jf_open_pro_camera_config.py human-track --action set --enable false
# 提高移动侦测灵敏度
python scripts/jf_open_pro_camera_config.py motion-detect --action set --enable true --level 5
```
### 场景 3: 查看当前配置
```bash
# 查看移动侦测配置
python scripts/jf_open_pro_camera_config.py motion-detect --action get
# 查看人形检测配置
python scripts/jf_open_pro_camera_config.py human-detection --action get
# 查看人形追踪配置
python scripts/jf_open_pro_camera_config.py human-track --action get
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 脚本会自动处理登录 |
---
## ⚠️ 注意事项
1. **设备需在线** - 操作前确保设备在线
2. **设备需登录** - 脚本会自动处理设备登录
3. **人形追踪限制** - 需要画面正放且能识别出人形才生效
4. **灵敏度调整** - 过高可能导致误报,过低可能漏报
5. **通道号** - 默认通道 0,NVR 设备可通过 `JF_CHANNEL` 环境变量指定
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
```bash
# 获取帮助
python scripts/jf_open_pro_camera_config.py --help
# 移动侦测
python scripts/jf_open_pro_camera_config.py motion-detect --action <get|set>
# 人形检测
python scripts/jf_open_pro_camera_config.py human-detection --action <get|set>
# 人形追踪
python scripts/jf_open_pro_camera_config.py human-track --action <get|set>
```
脚本路径:`scripts/jf_open_pro_camera_config.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:README.md
# jf-open-camera-config
JF 杰峰摄像机配置技能。
## 功能
- 移动侦测配置(布防/撤防/灵敏度 1-6)
- 人形检测配置(开关/灵敏度 0-3/检测类型)
- 人形追踪配置(开关/灵敏度 0-2/回位时间)
## 安装
将 `jf-open-camera-config.skill` 文件放入 OpenClaw 的 skills 目录:
```bash
cp jf-open-camera-config.skill ~/.openclaw/workspace/skills/
```
## 使用
### Node.js 版本
```bash
# 获取移动侦测配置
node scripts/jf-open-camera-config.js motion-detect \
--uuid <uuid> --appkey <appKey> --appsecret <appSecret> --sn <设备 SN> \
--action get
# 开启移动侦测(布防),灵敏度 3
node scripts/jf-open-camera-config.js motion-detect \
--uuid <uuid> --appkey <appKey> --appsecret <appSecret> --sn <设备 SN> \
--action set --enable true --level 3
```
### Python 版本
```bash
# 获取人形检测配置
python3 scripts/jf_open_camera_config.py human-detection \
--uuid <uuid> --appkey <appKey> --appsecret <appSecret> --sn <设备 SN> \
--action get
# 开启人形检测,灵敏度 2
python3 scripts/jf_open_camera_config.py human-detection \
--uuid <uuid> --appkey <appKey> --appsecret <appSecret> --sn <设备 SN> \
--action set --enable true --sensitivity 2
```
## 必需参数
| 参数 | 说明 |
|------|------|
| `--uuid` | 开放平台用户 uuid |
| `--appkey` | 开放平台应用 Key |
| `--appsecret` | 应用密钥 |
| `--sn` | 设备序列号 |
## 示例
```bash
# 开启所有检测功能
node scripts/jf-open-camera-config.js motion-detect \
--uuid xxx --appkey xxx --appsecret xxx --sn <SN> \
--action set --enable true --level 3
node scripts/jf-open-camera-config.js human-detection \
--uuid xxx --appkey xxx --appsecret xxx --sn <SN> \
--action set --enable true --sensitivity 2
node scripts/jf-open-camera-config.js human-track \
--uuid xxx --appkey xxx --appsecret xxx --sn <SN> \
--action set --enable true --sensitivity 1 --return-time 10
```
## 完整文档
详见 `SKILL.md`
## 许可证
MIT
FILE:scripts/jf_open_pro_camera_config.py
#!/usr/bin/env python3
"""
JF 杰峰摄像机配置工具 - Python 版本
仅支持环境变量配置凭据,避免命令行泄露风险。
用法:
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 jf_open_pro_camera_config.py motion-detect --action get
python jf_open_pro_camera_config.py motion-detect --action set --enable true --level 3
python jf_open_pro_camera_config.py human-detection --action set --enable true --sensitivity 2
python jf_open_pro_camera_config.py human-track --action set --enable true --sensitivity 1 --return-time 10
"""
import argparse
import hashlib
import json
import os
import time
import urllib.request
import urllib.error
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 get_signature(uuid, app_key, app_secret, time_millis, move_card=5):
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, move_card)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_time_millis():
return str(int(time.time() * 1000)).zfill(20)
def generate_request_id():
import random
return ''.join(random.choice('0123456789abcdef') for _ in range(32))
def https_post(url, data, headers):
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers=headers, method='POST')
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
raise Exception(f'HTTP 错误:{e.code} - {e.reason}')
except urllib.error.URLError as e:
raise Exception(f'请求失败:{e.reason}')
def get_device_token(config):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/token"
headers = {
'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis,
'signature': signature, 'X-Request-Id': generate_request_id()
}
data = {'sns': [config['deviceSn']], 'accessToken': ''}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取 Token 失败:{response.get('msg')}")
if not response.get('data') or len(response['data']) == 0:
raise Exception('获取 Token 失败:返回数据为空')
return response['data'][0]['token']
def get_motion_detect_config(config, device_token, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {'Name': 'Detect.MotionDetect'}
if channel > 0:
data['Channel'] = str(channel)
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取移动侦测配置失败:{response.get('msg')}")
return response['data']
def set_motion_detect_config(config, device_token, enable, level=3, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {
'Name': 'Detect.MotionDetect',
'Detect.MotionDetect': [{
'AlarmType': 0, 'Enable': enable, 'Level': level,
'Region': ['0xFFFFFFFF'] * 32,
'PIRCheckTime': 0, 'PirSensitive': 0,
'PirTimeSection': {'PirTimeSectionOne': {'Enable': False, 'WeekMask': 0}, 'PirTimeSectionTwo': {'Enable': False, 'WeekMask': 0}},
'EventHandler': {
'AlarmInfo': '', 'AlarmOutEnable': False, 'AlarmOutLatch': 10, 'AlarmOutMask': '0x00000000',
'BeepEnable': False, 'Dejitter': 0, 'EventLatch': 2, 'FTPEnable': False, 'LogEnable': False,
'MailEnable': False, 'MatrixEnable': False, 'MatrixMask': '0x00000000', 'MessageEnable': True,
'MsgtoNetEnable': False, 'PtzEnable': True, 'PtzLink': [['None', 0] for _ in range(64)],
'RecordEnable': True, 'RecordLatch': 30, 'RecordMask': '0x00000001', 'SnapEnable': True,
'SnapShotMask': '0x00000001',
'TimeSection': [['1 00:00:00-24:00:00'] + ['0 00:00:00-24:00:00'] * 5 for _ in range(7)],
'TipEnable': False, 'TourEnable': False, 'TourMask': '0x00000000', 'VoiceEnable': False, 'VoiceType': 520
}
}]
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设置移动侦测配置失败:{response.get('msg')}")
return response['data']
def get_human_detection_config(config, device_token, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {'Name': 'Detect.HumanDetection'}
if channel > 0:
data['Channel'] = str(channel)
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取人形检测配置失败:{response.get('msg')}")
return response['data']
def set_human_detection_config(config, device_token, enable, sensitivity=1, object_type=0, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {
'Name': 'Detect.HumanDetection',
'Detect.HumanDetection': [{
'Enable': enable, 'ObjectType': object_type, 'PedFdrAlg': 0, 'AlgoCreate': True,
'ShowRule': False, 'ShowTrack': False, 'Sensitivity': sensitivity, 'PushInterval': 3000,
'PedRule': [
{'Enable': False, 'RuleType': 1, 'RuleLine': {'AlarmDirect': 2, 'Pts': {'StartX': 100, 'StartY': 100, 'StopX': 8191, 'StopY': 8191}}, 'RuleRegion': {'AlarmDirect': 2, 'PtsNum': 4, 'Pts': [{'X': 100, 'Y': 100}, {'X': 8191, 'Y': 100}, {'X': 8191, 'Y': 8191}, {'X': 100, 'Y': 8191}]}},
{'Enable': False, 'RuleType': 1, 'RuleLine': {'AlarmDirect': 2, 'Pts': {'StartX': 100, 'StartY': 100, 'StopX': 8191, 'StopY': 8191}}, 'RuleRegion': {'AlarmDirect': 2, 'PtsNum': 4, 'Pts': [{'X': 100, 'Y': 100}, {'X': 8191, 'Y': 100}, {'X': 8191, 'Y': 8191}, {'X': 100, 'Y': 8191}]}},
{'Enable': False, 'RuleType': 1, 'RuleLine': {'AlarmDirect': 2, 'Pts': {'StartX': 100, 'StartY': 100, 'StopX': 8191, 'StopY': 8191}}, 'RuleRegion': {'AlarmDirect': 2, 'PtsNum': 4, 'Pts': [{'X': 100, 'Y': 100}, {'X': 8191, 'Y': 100}, {'X': 8191, 'Y': 8191}, {'X': 100, 'Y': 8191}]}},
{'Enable': False, 'RuleType': 1, 'RuleLine': {'AlarmDirect': 2, 'Pts': {'StartX': 100, 'StartY': 100, 'StopX': 8191, 'StopY': 8191}}, 'RuleRegion': {'AlarmDirect': 2, 'PtsNum': 4, 'Pts': [{'X': 100, 'Y': 100}, {'X': 8191, 'Y': 100}, {'X': 8191, 'Y': 8191}, {'X': 100, 'Y': 8191}]}}
]
}]
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设置人形检测配置失败:{response.get('msg')}")
return response['data']
def get_human_track_config(config, device_token, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {'Name': 'Detect.DetectTrack'}
if channel > 0:
data['Channel'] = str(channel)
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取人形追踪配置失败:{response.get('msg')}")
return response['data']
def set_human_track_config(config, device_token, enable, sensitivity=1, return_time=10, channel=0):
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {'uuid': config['uuid'], 'appKey': config['appKey'], 'timeMillis': time_millis, 'signature': signature, 'X-Request-Id': generate_request_id()}
data = {
'Name': 'Detect.DetectTrack',
'Detect.DetectTrack': {
'Enable': 1 if enable else 0,
'Sensitivity': sensitivity,
'ReturnTime': return_time
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设置人形追踪配置失败:{response.get('msg')}")
return response['data']
def parse_args():
parser = argparse.ArgumentParser(
description='JLink 杰峰摄像机配置工具 - Python 版本',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
JF_CHANNEL 通道号,默认 0 (可选)
示例:
# 设置环境变量
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 jf_open_pro_camera_config.py motion-detect --action get
# 移动侦测 - 开启(灵敏度 3)
python jf_open_pro_camera_config.py motion-detect --action set --enable true --level 3
# 人形检测 - 开启(灵敏度 2)
python jf_open_pro_camera_config.py human-detection --action set --enable true --sensitivity 2
# 人形追踪 - 开启(灵敏度 1,10 秒回位)
python jf_open_pro_camera_config.py human-track --action set --enable true --sensitivity 1 --return-time 10
''')
parser.add_argument('command', choices=['motion-detect', 'human-detection', 'human-track'], help='命令')
parser.add_argument('--action', choices=['get', 'set'], required=True, help='操作类型')
parser.add_argument('--enable', type=str, help='开启/关闭 (true/false)')
parser.add_argument('--level', type=int, help='移动侦测灵敏度 (1-6)')
parser.add_argument('--sensitivity', type=int, help='人形检测/追踪灵敏度')
parser.add_argument('--object-type', dest='object_type', type=int, help='检测目标类型 (0:人,1:物体)')
parser.add_argument('--return-time', dest='return_time', type=int, help='人形追踪回位时间 (秒)')
return parser.parse_args()
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)),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com'),
'deviceSn': os.environ.get('JF_SN'),
'channel': int(os.environ.get('JF_CHANNEL', 0))
}
def get_motion_level_text(level):
texts = ['', '最低', '较低', '中', '较高', '很高', '最高']
return texts[level] if 1 <= level <= 6 else '未知'
def get_human_sensitivity_text(sensitivity):
texts = ['低', '中', '高', '数量']
return texts[sensitivity] if 0 <= sensitivity <= 3 else '未知'
def get_ped_fdr_alg_text(alg):
texts = ['单人形检测', '人形 + 人脸检测', '人形 + 人脸识别', '人形 + 车形检测', '人形 + 车形 + 人脸检测', '宠物']
return texts[alg] if 0 <= alg <= 5 else '未知'
def main():
args = parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
return 1
try:
print('========================================')
title_map = {'motion-detect': '移动侦测', 'human-detection': '人形检测', 'human-track': '人形追踪'}
print(f"JLink {title_map.get(args.command, '')}配置")
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"通道号:{config['channel']}")
print(f"操作:{args.action}")
print()
device_token = get_device_token(config)
if args.command == 'motion-detect':
if args.action == 'get':
result = get_motion_detect_config(config, device_token, config['channel'])
motion_config = result.get('Detect.MotionDetect', [{}])[0]
if motion_config:
print('=== 移动侦测配置 ===')
print(f"开关状态:{'开启 (布防)' if motion_config.get('Enable') else '关闭 (撤防)'}")
print(f"灵敏度:{motion_config.get('Level')} ({get_motion_level_text(motion_config.get('Level', 0))})")
else:
if args.enable is None:
print('❌ 设置操作需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
level = args.level if args.level is not None else 3
if level < 1 or level > 6:
print('❌ 灵敏度必须在 1-6 之间')
return 1
result = set_motion_detect_config(config, device_token, enable, level, config['channel'])
print(f"✅ 移动侦测{'开启 (布防)' if enable else '关闭 (撤防)'}成功")
print(f"灵敏度:{level}")
print(f"返回码:{result.get('Ret')}")
elif args.command == 'human-detection':
if args.action == 'get':
result = get_human_detection_config(config, device_token, config['channel'])
human_config = result.get('Detect.HumanDetection', [{}])[0]
if human_config:
print('=== 人形检测配置 ===')
print(f"开关状态:{'开启' if human_config.get('Enable') else '关闭'}")
print(f"灵敏度:{human_config.get('Sensitivity')} ({get_human_sensitivity_text(human_config.get('Sensitivity', 0))})")
print(f"检测类型:{'人' if human_config.get('ObjectType') == 0 else '物体'}")
print(f"算法类型:{get_ped_fdr_alg_text(human_config.get('PedFdrAlg', 0))}")
else:
if args.enable is None:
print('❌ 设置操作需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
sensitivity = args.sensitivity if args.sensitivity is not None else 1
object_type = args.object_type if args.object_type is not None else 0
if sensitivity < 0 or sensitivity > 3:
print('❌ 灵敏度必须在 0-3 之间')
return 1
result = set_human_detection_config(config, device_token, enable, sensitivity, object_type, config['channel'])
print(f"✅ 人形检测{'开启' if enable else '关闭'}成功")
print(f"灵敏度:{sensitivity}")
print(f"检测类型:{'人' if object_type == 0 else '物体'}")
print(f"返回码:{result.get('Ret')}")
elif args.command == 'human-track':
if args.action == 'get':
result = get_human_track_config(config, device_token, config['channel'])
track_config = result.get('Detect.DetectTrack', {})
if track_config:
print('=== 人形追踪配置 ===')
print(f"开关状态:{'开启' if track_config.get('Enable') == 1 else '关闭'}")
print(f"灵敏度:{track_config.get('Sensitivity')} ({get_human_sensitivity_text(track_config.get('Sensitivity', 0))})")
return_time = track_config.get('ReturnTime', 0)
print(f"回位时间:{return_time}秒{' (不返回)' if return_time == 0 else ''}")
else:
if args.enable is None:
print('❌ 设置操作需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
sensitivity = args.sensitivity if args.sensitivity is not None else 1
return_time = args.return_time if args.return_time is not None else 10
if sensitivity < 0 or sensitivity > 2:
print('❌ 追踪灵敏度必须在 0-2 之间')
return 1
if return_time < 0 or return_time > 600:
print('❌ 回位时间必须在 0-600 秒之间')
return 1
result = set_human_track_config(config, device_token, enable, sensitivity, return_time, config['channel'])
print(f"✅ 人形追踪{'开启' if enable else '关闭'}成功")
print(f"灵敏度:{sensitivity}")
print(f"回位时间:{return_time}秒")
print(f"返回码:{result.get('Ret')}")
print('========================================')
return 0
except Exception as e:
print(f'❌ 错误:{e}')
return 1
if __name__ == '__main__':
exit(main())
面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
---
name: jf-open-pro-capture-livestream
description: 面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
# 必需凭证声明 - 平台元数据
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_USERNAME
type: string
description: 设备用户名
default: admin
- name: JF_PASSWORD
type: string
description: 设备密码
- name: JF_SN
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 # 仅支持 Python
---
# JF Open Pro Capture Livestream
> **面向开发者杰峰设备 API 工具 (Python)**
>
> 支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```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" # 设备序列号
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址
python scripts/jf_open_pro_capture_livestream.py livestream
# 获取 Token
python scripts/jf_open_pro_capture_livestream.py token
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USERNAME` | 设备用户名 | 否 | `admin` |
| `JF_PASSWORD` | 设备密码 | 否 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
| `JF_KEEPALIVE` | 保活时长(秒) | 否 | `300` |
---
## 🛠️ 功能
1. **获取设备 Token** - 通过设备序列号获取 24 小时有效的访问令牌
2. **设备登录认证** - 使用设备用户名/密码完成登录,获取 SessionID
3. **查询设备状态** - 获取设备在线状态、休眠状态、认证状态、IP 信息等
4. **设备云抓图** - 抓取设备实时图片(辅码流),图片地址有效期 24 小时
5. **获取直播地址** - 获取设备直播流地址(HLS/RTMP/FLV/WebRTC 等),默认有效期 10 小时
---
## 📖 详细文档
### 1. 获取设备 Token
**接口**: `POST /gwp/v3/rtc/device/token`
**响应**:
```json
{
"code": 2000,
"data": [{
"sn": "YOUR_DEVICE_SN",
"token": "ZTA3NTRiODMzNHw0OGRlOGMxYzFjMjBhNGEzfHwx..."
}]
}
```
**注意**: Token 有效期 24 小时,可缓存复用。
---
### 2. 查询设备状态
**接口**: `POST /gwp/v3/rtc/device/status`
**状态判定表**:
| status | wakeUpStatus | wakeUpEnable | 设备状态 |
|--------|--------------|--------------|----------|
| online | 空 | 空 | 常电设备,在线 |
| online | 0 | 1 | 低功耗设备,已休眠 |
| online | 1 | 1 | 低功耗设备,已唤醒 |
| online | 2 | 1 | 低功耗设备,准备休眠中 |
| notfound | 空 | 空 | 设备不在线 |
---
### 3. 设备云抓图
**接口**: `POST /gwp/v3/rtc/device/capture/{deviceToken}`
**注意**:
- ⚠️ **按调用次数计费** - 详见官网定价
- ⚠️ **图片有效期 24 小时** - 过期自动清除,需及时下载
---
### 4. 获取直播地址
**接口**: `POST /gwp/v3/rtc/device/livestream/{deviceToken}`
**支持协议**:
| 协议 | 参数 | 适用场景 |
|------|------|----------|
| HLS | `hls-ts` | Web 浏览器、移动端(推荐) |
| FLV | `flv` | Web 播放器 |
| WebRTC | `webrtc` | 超低延迟(仅 H.264) |
| RTMP | `rtmp-flv` | 微信小程序 |
**注意**:
- ⚠️ **直播地址默认有效期 10 小时**
- ⚠️ **低功耗设备** - 获取后 3 秒内必须播放
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 调用 login 接口登录 |
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
FILE:README.md
# JF Open Capture Livestream 技能
JF 杰峰智能设备鉴权与状态查询 AgentSkills (Python)。
---
## 📋 必需凭据
**使用前必须设置以下环境变量:**
| 参数 | 环境变量 | 类型 | 说明 | 来源 |
|------|----------|------|------|------|
| `uuid` | `JF_UUID` | string | 开放平台用户唯一标识 | 杰峰开放平台 |
| `appKey` | `JF_APPKEY` | string | 开放平台应用 Key | 杰峰开放平台 |
| `appSecret` | `JF_APPSECRET` | string | 应用密钥 | 杰峰开放平台 |
| `moveCard` | `JF_MOVECARD` | int | 签名算法偏移量 (0-9) | 杰峰开放平台 |
| `deviceSn` | `JF_SN` | string | 设备序列号 | 设备标签 |
⚠️ **如果缺少以上凭据,此技能无法正常工作!**
---
## 🔒 安全说明
**仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 目录结构
```
jf-open-pro-capture-livestream/
├── SKILL.md # 技能文档
├── README.md # 使用说明
└── scripts/
├── jf_open_pro_capture_livestream.py # Python SDK
└── requirements.txt # Python 依赖
```
---
## 快速开始
### 1. 设置环境变量
```bash
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"
```
### 2. 安装依赖
```bash
pip install -r scripts/requirements.txt
```
### 3. 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址(HLS 协议)
python scripts/jf_open_pro_capture_livestream.py livestream
```
---
## 功能
- ✅ 获取设备 Token(24 小时有效)
- ✅ 设备登录认证
- ✅ 查询设备状态(在线/离线/休眠)
- ✅ 自动签名计算
- ✅ 设备云抓图(图片有效期 24 小时)
- ✅ 获取直播地址(有效期 10 小时)
---
## 可用命令
| 命令 | 说明 |
|------|------|
| `status` | 查询设备状态 |
| `login` | 设备登录认证 |
| `capture` | 设备云抓图 |
| `livestream` | 获取直播地址 |
| `token` | 仅获取设备 Token |
---
## 依赖
- **Python:** 3.7+ (需要 `requests` 库)
---
## 文档
- `SKILL.md` - 完整技能文档
- `README.md` - 快速开始指南
FILE:scripts/jf_open_pro_capture_livestream.py
#!/usr/bin/env python3
"""
JF 杰峰设备认证与状态查询 Python SDK
功能:
- 获取设备 Token
- 设备登录
- 查询设备状态
- 设备云抓图
- 获取直播地址
用法:
# 设置环境变量
export JF_UUID="xxx" JF_APPKEY="xxx" JF_APPSECRET="xxx" JF_MOVECARD=5 JF_SN="xxx"
python jf_open_pro_capture_livestream.py status
安全说明:
✅ 仅支持环境变量,避免凭据泄露风险
🔒 不支持命令行参数或配置文件
"""
import hashlib
import os
import sys
import time
try:
import requests
except ImportError:
print("❌ 错误:需要安装 requests 库")
print("请运行:pip install -r requirements.txt")
sys.exit(1)
# ==================== 配置 ====================
DEFAULT_ENDPOINT = 'api.jftechws.com'
DEFAULT_MOVECARD = 5
def get_config():
"""
从环境变量获取配置
必需环境变量:
JF_UUID - 开放平台用户唯一标识
JF_APPKEY - 开放平台应用 Key
JF_APPSECRET - 开放平台应用密钥
JF_MOVECARD - 签名算法偏移量 (0-9)
JF_SN - 设备序列号
可选环境变量:
JF_USERNAME - 设备用户名 (默认 admin)
JF_PASSWORD - 设备密码
JF_ENDPOINT - API 端点 (默认 api.jftechws.com)
JF_KEEPALIVE - 保活时长 (默认 300)
"""
config = {
'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', DEFAULT_MOVECARD)),
'endpoint': os.environ.get('JF_ENDPOINT', DEFAULT_ENDPOINT),
'deviceSn': os.environ.get('JF_SN', ''),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', ''),
'keepaliveTime': int(os.environ.get('JF_KEEPALIVE', 300)),
}
return config
# ==================== JF 杰峰认证 SDK ====================
class JFAuth:
"""JF 杰峰设备认证 SDK"""
def __init__(self, uuid, app_key, app_secret, move_card, endpoint=DEFAULT_ENDPOINT):
"""
初始化 JF 认证
Args:
uuid: 开放平台用户 uuid(必需)
app_key: 开放平台应用 appKey(必需)
app_secret: 应用密钥(必需)
move_card: 签名算法参数(必需,int 类型 0-9)
endpoint: API 端点域名
"""
if not all([uuid, app_key, app_secret, move_card]):
raise ValueError("缺少必需的配置参数:uuid, app_key, app_secret, move_card")
self.uuid = uuid
self.app_key = app_key
self.app_secret = app_secret
self.endpoint = endpoint
self.move_card = move_card
def _str2byte(self, s):
"""字符串转字节数组(ISO-8859-1 编码)"""
return list(s.encode('iso-8859-1'))
def _change(self, encrypt_str, move_card):
"""简单移位算法"""
encrypt_byte = self._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(self, encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for i in range(length):
temp[i] = encrypt_byte[i]
temp[length * 2 - 1 - i] = change_byte[i]
return temp
def _get_signature(self, time_millis):
"""生成签名"""
encrypt_str = self.uuid + self.app_key + self.app_secret + time_millis
encrypt_byte = self._str2byte(encrypt_str)
change_byte = self._change(encrypt_str, self.move_card)
merged_byte = self._merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def _get_time_millis(self):
"""生成 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def _generate_request_id(self):
"""生成请求 ID"""
import uuid
return uuid.uuid4().hex
def _request(self, url, body, headers=None):
"""发送 HTTP 请求"""
if headers is None:
headers = {}
headers['Content-Type'] = 'application/json'
try:
response = requests.post(url, headers=headers, json=body, timeout=30)
return response.json()
except Exception as e:
return {'code': 0, 'msg': str(e)}
def get_device_token(self, device_sn):
"""
获取设备 Token(24 小时有效)
"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/token"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'sns': [device_sn], 'accessToken': ''}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
return {'success': True, 'token': result['data'][0]['token'], 'sn': result['data'][0]['sn']}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_login(self, device_token, username, password='', keepalive_time=300):
"""设备登录认证"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'UserName': username,
'PassWord': password,
'KeepaliveTime': keepalive_time
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {
'success': True,
'sessionId': result['data'].get('SessionID'),
'deviceType': result['data'].get('DeviceType'),
'aliveInterval': result['data'].get('AliveInterval'),
'channelNum': result['data'].get('ChannelNum')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def get_device_status(self, device_token):
"""查询设备状态"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/status"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'deviceTokenList': [device_token], 'region': 'Local'}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
device = result['data'][0]
status = device.get('status', 'unknown')
status_desc = '未知'
if status == 'online':
wake_status = device.get('wakeUpStatus')
if wake_status is None:
status_desc = '常电设备,在线'
elif wake_status == '0':
status_desc = '低功耗设备,已休眠'
elif wake_status == '1':
status_desc = '低功耗设备,已唤醒'
elif wake_status == '2':
status_desc = '低功耗设备,准备休眠中'
elif status == 'notfound':
status_desc = '设备不在线'
auth_status = device.get('authStatus')
auth_desc = '未知'
if auth_status is not None:
if auth_status == 1:
auth_desc = '认证成功'
elif auth_status == 0:
auth_desc = '正在认证'
elif auth_status == -1:
auth_desc = '认证未通过'
return {
'success': True,
'uuid': device.get('uuid'),
'status': status,
'statusDesc': status_desc,
'authStatus': auth_status,
'authDesc': auth_desc,
'wakeUpStatus': device.get('wakeUpStatus'),
'wakeUpEnable': device.get('wakeUpEnable'),
'wanIp': device.get('wanIp'),
'channel': device.get('channel')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_capture(self, device_token, channel=0, pic_type=0):
"""设备云抓图"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/capture/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'Name': 'OPSNAP',
'OPSNAP': {'Channel': channel, 'PicType': pic_type}
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'imageUrl': result['data'].get('image'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret')}
def get_live_stream(self, device_token, channel='0', stream='1', protocol='flv', username='admin', password='', expire_time=None):
"""获取直播地址"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/livestream/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'channel': channel,
'stream': stream,
'protocol': protocol,
'username': username,
'password': password
}
if expire_time:
body['expireTime'] = expire_time
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'url': result['data'].get('url'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret'), 'retMsg': result.get('data', {}).get('retMsg')}
# ==================== 输出函数 ====================
def print_device_status(status):
"""打印设备状态"""
print("\n=== 设备状态 ===")
print(f"设备序列号:{status['uuid']}")
print(f"状态:{status['status']} ({status['statusDesc']})")
if status.get('authDesc'):
print(f"认证状态:{status['authDesc']} ({status['authStatus']})")
if status.get('wakeUpStatus') is not None:
print(f"休眠状态:{status['wakeUpStatus']}")
if status.get('wakeUpEnable') is not None:
print(f"远程唤醒:{'支持' if status['wakeUpEnable'] == '1' else '不支持'}")
if status.get('wanIp'):
print(f"外网 IP: {status['wanIp']}")
# ==================== 主函数 ====================
def main():
if len(sys.argv) < 2:
print("用法:python jf_open_pro_capture_livestream.py <command>")
print("")
print("可用命令:")
print(" status 查询设备状态")
print(" login 设备登录")
print(" capture 设备云抓图")
print(" livestream 获取直播地址")
print(" token 仅获取设备 Token")
print("")
print("环境变量:")
print(" JF_UUID - 开放平台用户唯一标识(必需)")
print(" JF_APPKEY - 开放平台应用 Key(必需)")
print(" JF_APPSECRET - 开放平台应用密钥(必需)")
print(" JF_MOVECARD - 签名算法偏移量 (0-9)(必需)")
print(" JF_SN - 设备序列号(必需)")
print(" JF_USERNAME - 设备用户名(可选,默认 admin)")
print(" JF_PASSWORD - 设备密码(可选)")
print(" JF_ENDPOINT - API 端点(可选,默认 api.jftechws.com)")
sys.exit(1)
command = sys.argv[1]
# 从环境变量获取配置
config = get_config()
# 验证必需参数
if not config['uuid']:
print("❌ 错误:缺少必需环境变量 JF_UUID")
sys.exit(1)
if not config['appKey']:
print("❌ 错误:缺少必需环境变量 JF_APPKEY")
sys.exit(1)
if not config['appSecret']:
print("❌ 错误:缺少必需环境变量 JF_APPSECRET")
sys.exit(1)
if not config['deviceSn']:
print("❌ 错误:缺少必需环境变量 JF_SN")
sys.exit(1)
print("============================================================")
print("JF 杰峰设备认证工具 (Python)")
print("============================================================")
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{command}")
# 初始化 SDK
sdk = JFAuth(
uuid=config['uuid'],
app_key=config['appKey'],
app_secret=config['appSecret'],
move_card=config['moveCard'],
endpoint=config['endpoint']
)
device_token = None
try:
if command == 'token':
print("\n>>> 获取设备 Token...")
result = sdk.get_device_token(config['deviceSn'])
if result['success']:
print(f"✅ Token: {result['token']}")
else:
print(f"❌ 失败:{result['error']}")
elif command == 'status':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 查询设备状态...")
status_result = sdk.get_device_status(device_token)
if status_result['success']:
print_device_status(status_result)
else:
print(f"❌ 查询失败:{status_result['error']}")
elif command == 'login':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 设备登录...")
print(f"用户名:{config['userName']}, 保活时长:{config['keepaliveTime']}秒")
login_result = sdk.device_login(device_token, config['userName'], config['passWord'], config['keepaliveTime'])
if login_result['success']:
print("\n=== 登录成功 ===")
print(f"SessionID: {login_result['sessionId']}")
print(f"设备类型:{login_result['deviceType']}")
print(f"保活间隔:{login_result['aliveInterval']}秒")
print(f"通道数:{login_result['channelNum']}")
else:
print(f"❌ 登录失败:{login_result['error']}")
elif command == 'capture':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 执行云抓图...")
print("通道号:0, 图片类型:实时图片(辅码流)")
print("⚠️ 注意:云抓图按调用次数计费")
capture_result = sdk.device_capture(device_token, 0, 0)
if capture_result['success']:
print("\n=== 抓图成功 ===")
print(f"图片地址:{capture_result['imageUrl']}")
print("⚠️ 图片有效期 24 小时,请及时下载!")
print(f"\n下载命令:curl -o snapshot.png \"{capture_result['imageUrl']}\"")
else:
print(f"❌ 抓图失败:{capture_result['error']}")
if capture_result.get('ret'):
print(f"设备返回码:{capture_result['ret']}")
elif command == 'livestream':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 获取直播地址...")
print("通道号:0, 码流:标清(辅码流), 协议:flv")
print("⚠️ 注意:直播地址默认有效期 10 小时")
print("⚠️ 低功耗设备:获取后 3 秒内必须播放")
stream_result = sdk.get_live_stream(
device_token,
'0',
'1',
'flv',
config['userName'],
config['passWord']
)
if stream_result['success']:
print("\n=== 直播地址获取成功 ===")
print(f"播放地址:{stream_result['url']}")
print("\n使用方式:")
print(" - H5 播放:<video src=\"URL\" controls></video>")
print(" - VLC 播放:vlc \"URL\"")
print(" - ffmpeg: ffmpeg -i \"URL\" output.mp4")
print("\n⚠️ 地址有效期 10 小时,可重复使用")
else:
print(f"❌ 获取失败:{stream_result['error']}")
if stream_result.get('retMsg'):
print(f"设备信息:{stream_result['retMsg']}")
if stream_result.get('ret'):
print(f"设备返回码:{stream_result['ret']}")
else:
print(f"❌ 未知命令:{command}")
print("可用命令:status, login, capture, livestream, token")
except Exception as e:
print(f"❌ 执行出错:{e}")
import traceback
traceback.print_exc()
print("\n============================================================")
if __name__ == '__main__':
main()
FILE:scripts/requirements.txt
# JLink Python SDK 依赖
requests>=2.28.0