Hsa test2
萤石语音广播技能。支持本地音频文件上传或文本转语音,实现语音内容下发到设备播放。 Use when: 需要向萤石设备发送语音通知、广播、提醒等音频内容。 ⚠️ 安全要求:必须设置 EZVIZ_APP_KEY 和 EZVIZ_APP_SECRET 环境变量,使用最小权限凭证。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install skilldb:shuanhu95~hsa-test2cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Ashuanhu95~hsa-test2/file -o hsa-test2.mdGit 仓库获取源码
git clone https://github.com/openclaw/skills/commit/23a14d23a0e396c0aca4284bf337e15f354dab69# Ezviz Audio Broadcast (萤石语音广播)
通过萤石语音上传和下发接口,实现语音内容到设备的广播播放。
---
## ⚠️ 安全警告 (安装前必读)
**在使用此技能前,请完成以下安全检查:**
| # | 检查项 | 状态 | 说明 |
|---|--------|------|------|
| 1 | **凭证权限** | ⚠️ 必需 | 使用**最小权限**的 AppKey/AppSecret,不要用主账号凭证 |
| 2 | **配置文件** | ⚠️ 注意 | 技能会读取 `~/.openclaw/config.json` 等文件中的萤石凭证 |
| 3 | **Token 缓存** | ⚠️ 注意 | Token 缓存在 `/tmp/ezviz_global_token_cache/` (权限 600) |
| 4 | **系统命令** | ⚠️ 注意 | 使用 `say`/`espeak`/`ffmpeg` 进行 TTS (通过 subprocess) |
| 5 | **API 域名** | ✅ 已验证 | `openai.ys7.com` 是萤石官方 API 端点 |
| 6 | **代码审查** | ✅ 推荐 | 审查 `scripts/audio_broadcast.py` 和 `lib/token_manager.py` |
### 快速安全配置
```bash
# 1. 使用环境变量(优先级最高,避免配置文件意外使用)
export EZVIZ_APP_KEY="your_dedicated_app_key"
export EZVIZ_APP_SECRET="your_dedicated_app_secret"
export EZVIZ_DEVICE_SERIAL="device1,device2"
# 2. 高安全环境:禁用 Token 缓存
export EZVIZ_TOKEN_CACHE=0
# 3. 测试凭证(推荐先用测试账号)
# 登录 https://openai.ys7.com/ 创建专用应用,仅开通语音相关权限
```
### 凭证优先级
技能按以下顺序获取凭证(**优先级从高到低**):
1. **环境变量** (`EZVIZ_APP_KEY`, `EZVIZ_APP_SECRET`) ← 推荐
2. **Channels 配置** (`~/.openclaw/config.json` 等)
3. **命令行参数** (直接传入)
---
## 快速开始
### 安装依赖
```bash
pip install requests
```
### 设置环境变量
```bash
export EZVIZ_APP_KEY="your_app_key"
export EZVIZ_APP_SECRET="your_app_secret"
export EZVIZ_DEVICE_SERIAL="dev1,dev2,dev3"
```
可选环境变量:
```bash
export EZVIZ_CHANNEL_NO="1" # 通道号,默认 1
export EZVIZ_AUDIO_FILE="/path/to/audio.mp3" # 本地音频文件路径(二选一)
export EZVIZ_TEXT_CONTENT="语音内容文本" # 文本内容(二选一)
export EZVIZ_VOICE_NAME="custom_name" # 自定义语音名称
export EZVIZ_TOKEN_CACHE="1" # Token 缓存:1=启用 (默认), 0=禁用
```
**Token 缓存说明**:
- ✅ **默认启用**: 技能默认使用 Token 缓存,提升效率
- ⚠️ **禁用缓存**: 设置 `EZVIZ_TOKEN_CACHE=0` 每次重新获取 Token
- 📁 **缓存位置**: `/tmp/ezviz_global_token_cache/global_token_cache.json`
- 🔒 **文件权限**: 600 (仅所有者可读写)
- ⏰ **有效期**: 7 天,到期前 5 分钟自动刷新
**注意**:
- 不需要设置 `EZVIZ_ACCESS_TOKEN`!技能会自动获取 Token
- 必须提供 `EZVIZ_AUDIO_FILE` 或 `EZVIZ_TEXT_CONTENT` 其中一个
- 设备需要支持对讲功能(`support_talk=1或3`)
### 运行
```bash
python3 {baseDir}/scripts/audio_broadcast.py
```
命令行参数:
```bash
# 使用本地音频文件
python3 {baseDir}/scripts/audio_broadcast.py appKey appSecret dev1 /path/to/audio.mp3 [channel_no]
# 使用文本内容(自动生成语音)
python3 {baseDir}/scripts/audio_broadcast.py appKey appSecret dev1 "语音内容文本" [channel_no]
```
## 工作流程
```
1. 获取 Token (appKey + appSecret → accessToken)
↓
2a. 如果提供音频文件:直接上传文件
2b. 如果提供文本:调用TTS生成音频文件,然后上传
↓
3. 下发语音 (accessToken + deviceSerial + fileUrl → 设备播放)
↓
4. 输出结果 (JSON + 控制台)
```
## Token 自动获取说明
**你不需要手动获取或配置 `EZVIZ_ACCESS_TOKEN`!**
技能会自动处理 Token 的获取:
```
首次运行:
appKey + appSecret → 调用萤石 API → 获取 accessToken (有效期 7 天)
↓
保存到缓存文件(系统临时目录)
↓
后续运行:
检查缓存 Token 是否过期
├─ 未过期 → 直接使用缓存 Token ✅
└─ 已过期 → 重新获取新 Token
```
**Token 管理特性**:
- ✅ **自动获取**: 首次运行自动调用萤石 API 获取
- ✅ **有效期 7 天**: 获取的 Token 7 天内有效
- ✅ **智能缓存**: Token 有效期内不重复获取,提升效率
- ✅ **安全缓冲**: 到期前 5 分钟自动刷新,避免边界问题
- ✅ **无需配置**: 不需要手动设置 `EZVIZ_ACCESS_TOKEN` 环境变量
- ✅ **安全存储**: 缓存文件存储在系统临时目录,权限 600
- ⚠️ **可选禁用**: 设置 `EZVIZ_TOKEN_CACHE=0` 可禁用缓存(每次运行重新获取)
## 输出示例
```
======================================================================
Ezviz Audio Broadcast Skill (萤石语音广播)
======================================================================
[Time] 2026-03-16 16:30:00
[INFO] Target devices: 2
- dev1 (Channel: 1)
- dev2 (Channel: 1)
[INFO] Mode: Text-to-Speech
[INFO] Content: 接下来插播一个广告
======================================================================
[Step 1] Getting access token...
[SUCCESS] Token obtained, expires: 2026-03-23 16:30:00
======================================================================
[Step 2] Generating and uploading audio...
[INFO] Generated TTS file: /tmp/ezviz_tts_12345.mp3
[SUCCESS] Audio uploaded successfully!
[INFO] Voice Name: ad_broadcast
[INFO] File URL: https://oss-cn-shenzhen.aliyuncs.com/voice/...
======================================================================
[Step 3] Broadcasting audio to devices...
======================================================================
[Device] dev1 (Channel: 1)
[SUCCESS] Audio broadcast completed!
[Device] dev2 (Channel: 1)
[SUCCESS] Audio broadcast completed!
======================================================================
BROADCAST SUMMARY
======================================================================
Total devices: 2
Success: 2
Failed: 0
======================================================================
```
## 多设备格式
| 格式 | 示例 | 说明 |
|------|------|------|
| 单设备 | `dev1` | 默认通道 1 |
| 多设备 | `dev1,dev2,dev3` | 全部使用默认通道 |
| 指定通道 | `dev1:1,dev2:2` | 每个设备独立通道 |
| 混合 | `dev1,dev2:2,dev3` | 部分指定通道 |
## API 接口
| 接口 | URL | 文档 |
|------|-----|------|
| 获取 Token | `POST /api/lapp/token/get` | https://openai.ys7.com/help/81 |
| 语音上传 | `POST /api/lapp/voice/upload` | https://openai.ys7.com/help/1241 |
| 语音下发 | `POST /api/lapp/voice/send` | https://openai.ys7.com/help/1253 |
## 网络端点
| 域名 | 用途 |
|------|------|
| `openai.ys7.com` | 萤石开放平台 API(Token、语音上传、下发) |
## 音频要求
| 格式 | 支持 | 限制 |
|------|------|------|
| WAV | ✅ | 最大5MB,最长60秒 |
| MP3 | ✅ | 最大5MB,最长60秒 |
| AAC | ✅ | 最大5MB,最长60秒 |
## 注意事项
⚠️ **设备兼容性**: 设备必须支持对讲功能(`support_talk=1或3`),否则返回错误码 `20015`
⚠️ **频率限制**: 语音下发接口有调用频率限制,建议设备间间隔1秒以上
⚠️ **文件大小**: 音频文件不能超过5MB,时长不能超过60秒
⚠️ **Token 安全**: Token 缓存到系统临时目录(权限 600),不写入日志,不发送到非萤石端点
⚠️ **TTS依赖**: 文本转语音功能依赖系统TTS服务,确保系统支持
## 数据流出说明
**本技能会向第三方服务发送数据**:
| 数据类型 | 发送到 | 用途 | 是否必需 |
|----------|--------|------|----------|
| 音频文件 | `openai.ys7.com` (萤石) | 语音上传和下发 | ✅ 必需 |
| appKey/appSecret | `openai.ys7.com` (萤石) | 获取访问 Token | ✅ 必需 |
| 设备序列号 | `openai.ys7.com` (萤石) | 请求下发 | ✅ 必需 |
| **EZVIZ_ACCESS_TOKEN** | **自动生成** | **每次运行自动获取** | **✅ 自动** |
**数据流出说明**:
- ✅ **萤石开放平台** (`openai.ys7.com`): 所有API调用 - 萤石官方 API
- ❌ **无其他第三方**: 不会发送数据到其他服务
**凭证权限建议**:
- 使用**最小权限**的 appKey/appSecret
- 仅开通必要的 API 权限(语音上传、下发)
- 定期轮换凭证
- 不要使用主账号凭证
**本地处理**:
- ✅ Token 缓存到系统临时目录(权限 600)
- ✅ TTS临时文件自动清理
- ✅ 不记录完整 API 响应
## 应用场景
- ✅ 可选禁用缓存:设置 EZVIZ_TOKEN_CACHE=0
| 场景 | 说明 |
|------|------|
| 📢 安全广播 | 向监控区域发送安全提醒、紧急通知 |
| 🏢 办公通知 | 办公室广播会议提醒、访客通知 |
| 🏪 商业应用 | 商店促销广播、排队叫号 |
| 🏠 智能家居 | 家庭语音提醒、门铃通知 |
| 🏭 工厂管理 | 生产线通知、安全警示 |
## 使用示例
**场景1: 紧急安全通知**
```bash
python3 audio_broadcast.py your_key your_secret "kitchen_cam,dining_area" "注意!检测到安全隐患,请立即检查!"
```
**场景2: 商业促销广播**
```bash
export EZVIZ_TEXT_CONTENT="欢迎光临!今日特价商品限时优惠,详情请咨询店员!"
export EZVIZ_DEVICE_SERIAL="store_front,store_back"
python3 audio_broadcast.py
```
**场景3: 使用预录制音频**
```bash
python3 audio_broadcast.py your_key your_secret entrance_cam /path/to/welcome_message.mp3
```
---
## API 端点
| 接口 | URL | 文档 |
|------|-----|------|
| 获取 Token | `POST /api/lapp/token/get` | https://openai.ys7.com/help/81 |
| 语音上传 | `POST /api/lapp/voice/upload` | https://openai.ys7.com/help/1241 |
| 语音下发 | `POST /api/lapp/voice/send` | https://openai.ys7.com/help/1253 |
**API 域名**: `https://openai.ys7.com` (萤石开放平台)
### ✅ 域名验证
```bash
# 验证域名连通性
curl -I https://openai.ys7.com/api/lapp/token/get
# 检查 SSL 证书
curl -vI https://openai.ys7.com/api/lapp/token/get 2>&1 | grep -A5 "SSL certificate"
# 验证域名所有权(萤石开放平台)
whois ys7.com
```
**官方文档**: https://open.ys7.com/
### ⚠️ 配置文件扫描说明
技能会读取以下路径中的萤石配置(仅当环境变量未设置时):
```
~/.openclaw/config.json
~/.openclaw/gateway/config.json
~/.openclaw/channels.json
```
**配置格式**:
```json
{
"channels": {
"ezviz": {
"appId": "your_app_id",
"appSecret": "your_app_secret",
"domain": "https://openai.ys7.com",
"enabled": true
}
}
}
```
**安全建议**:
- ✅ 使用**专用萤石凭证**,不要与其他服务共享
- ✅ 如果不想使用配置文件扫描,设置环境变量覆盖
- ✅ 定期审查配置文件中的凭证权限
- ❌ 不要在配置文件中存储主账号凭证
**禁用配置文件扫描**(环境变量优先):
```bash
export EZVIZ_APP_KEY="your_key"
export EZVIZ_APP_SECRET="your_secret"
# 环境变量优先级高于配置文件
```
---
## 系统依赖
**Python 包**:
```bash
pip install requests
```
**系统工具** (仅 TTS 需要):
- macOS: `say`, `afconvert`
- Linux: `espeak`, `ffmpeg`
### ⚠️ 系统命令安全说明
技能通过 `subprocess` 调用系统 TTS 命令:
```python
# macOS TTS
subprocess.run(['say', '-o', temp_aiff, text_with_pauses], check=True, capture_output=True)
subprocess.run(['afconvert