---
name: ezviz-audio-broadcast
description: |
  萤石语音广播技能。支持本地音频文件上传或文本转语音，实现语音内容下发到设备播放。
  Use when: 需要向萤石设备发送语音通知、广播、提醒等音频内容。
  
  ⚠️ 安全要求：必须设置 EZVIZ_APP_KEY 和 EZVIZ_APP_SECRET 环境变量，使用最小权限凭证。
metadata:
  openclaw:
    emoji: "🔊"
    requires:
      env: ["EZVIZ_APP_KEY", "EZVIZ_APP_SECRET", "EZVIZ_DEVICE_SERIAL"]
      pip: ["requests"]
    primaryEnv: "EZVIZ_APP_KEY"
    warnings:
      - "Requires Ezviz credentials with minimal permissions"
      - "Token cached in system temp directory (configurable)"
      - "Uses system TTS commands (say/espeak/ffmpeg) via subprocess"
    config:
      tokenCache:
        default: true
        envVar: "EZVIZ_TOKEN_CACHE"
        description: "Enable token caching (default: true). Set to 0 to disable."
---

# 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', '-f', 'WAVE', '-d', 'LEI16@44100', temp_aiff, output_path], ...)

# Linux TTS
subprocess.run(['espeak', '-w', output_path, text], check=True, capture_output=True)
subprocess.run(['ffmpeg', '-i', input, '-acodec', 'libmp3lame', output], ...)
```

**安全措施** (v1.0.2+):
- ✅ 使用**列表参数**调用 subprocess（避免 shell 注入）
- ✅ **输入验证**: 文本内容经过 `validate_text_input()` 验证
- ✅ **危险字符拦截**: 拒绝包含 `;`, `|`, `&`, `$()`, 等字符的输入
- ✅ **长度限制**: 文本内容最多 500 字符
- ✅ 临时文件存储在系统临时目录，使用后清理

**输入验证规则**:
| 检查项 | 限制 | 说明 |
|--------|------|------|
| 空内容 | ❌ 拒绝 | 文本不能为空 |
| 长度 | ≤500 字符 | 防止过长输入 |
| 危险字符 | ❌ 拒绝 | `;`, `|`, `&`, `` ` ``, `$()`, `${}` 等 |
| 设备序列号 | 字母数字 | 只允许 `A-Za-z0-9_:,` |

**安全建议**:
- ✅ 确保系统二进制文件来自可信源
- ✅ 在高安全环境运行前审查 `text_to_speech()` 函数
- ✅ 使用容器/沙箱隔离执行环境
- ✅ 输入验证提供额外保护层（防御纵深）

**不使用 TTS** (仅提供音频文件，避免系统命令):
```bash
python3 scripts/audio_broadcast.py appKey appSecret deviceSerial --audio-file /path/to/audio.mp3
```

---

## 安全说明

### ⚠️ 环境变量声明

**必需环境变量** (在 SKILL.md metadata 中声明):
```yaml
metadata:
  openclaw:
    requires:
      env: ["EZVIZ_APP_KEY", "EZVIZ_APP_SECRET", "EZVIZ_DEVICE_SERIAL"]
```

**⚠️ 注意**: 如果 Registry metadata 未显示必需环境变量，以 SKILL.md 为准。

### ⚠️ Token 持久化说明

**默认行为**:
- Token 会缓存到系统临时目录（`/tmp/ezviz_global_token_cache/global_token_cache.json`）
- 缓存有效期 7 天，文件权限 600（仅所有者可读写）
- 所有萤石技能共享同一个全局缓存

**缓存位置查询**:
```bash
# macOS
ls -la /var/folders/*/T/ezviz_global_token_cache/

# Linux
ls -la /tmp/ezviz_global_token_cache/

# 查看缓存内容（脱敏）
cat /tmp/ezviz_global_token_cache/global_token_cache.json
```

**⚠️ 潜在风险**:
| 风险 | 等级 | 说明 |
|------|------|------|
| Token 明文存储 | 🟡 中 | 文件权限 600，但仍是明文 |
| 跨技能共享 | 🟡 中 | 同一用户的技能可共享 Token |
| 多用户系统 | 🟠 高 | 其他用户可能访问临时目录 |
| 临时目录清理 | 🟢 低 | 系统重启可能清理临时文件 |

**禁用缓存** (高安全环境/共享系统):
```bash
export EZVIZ_TOKEN_CACHE=0
python3 scripts/audio_broadcast.py ...
```

**清除缓存**:
```bash
# 清除所有缓存 Token
rm -rf /tmp/ezviz_global_token_cache/

# 或使用 Token 管理器
python3 lib/token_manager.py clear
```

**安全建议**:
| 环境 | 建议 |
|------|------|
| 单用户个人设备 | ✅ 默认缓存安全（文件权限 600） |
| 多用户共享系统 | ⚠️ 设置 `EZVIZ_TOKEN_CACHE=0` |
| 高安全环境 | ⚠️ 禁用缓存 + 专用 OS 用户 + 容器隔离 |
| 生产环境 | ⚠️ 容器/VM 隔离 + 禁用缓存 + 密钥管理器 |

### ⚠️ 凭证轮换通知

**⚠️ 早期版本用户必须轮换凭证！**

如果您曾经使用过旧版本的 .env 文件:
```
🚨 您的凭证可能已泄露！

立即执行:
1. 登录 https://openai.ys7.com/
2. 删除旧应用，创建新应用
3. 获取新的 AppKey 和 AppSecret
```

**正确做法**:
- ❌ 不要使用 .env 文件（已删除）
- ✅ 使用环境变量
- ✅ 使用最小权限凭证
- ✅ 定期轮换凭证（建议每 90 天）
- ✅ 使用密钥管理器（生产环境）

### 数据流出说明

| 数据类型 | 发送到 | 用途 |
|----------|--------|------|
| AppKey/AppSecret | `openai.ys7.com` | 获取 Token |
| 设备序列号 | `openai.ys7.com` | API 调用 |
| 音频文件 | `openai.ys7.com` | 语音上传 |

---

## 代码审查

**推荐审查的文件**:
- `scripts/audio_broadcast.py` - 主脚本
- `lib/token_manager.py` - Token 管理器

**代码特点**:
- ✅ 代码未混淆，易于审查
- ✅ 使用 `requests` 库（标准做法）
- ✅ 使用 `subprocess` 带列表参数（减少 shell 注入风险）
- ⚠️ TTS 调用系统二进制文件（say/espeak/ffmpeg）

---

## 降低风险操作清单

**安装前**:
- [ ] 审查所有代码（scripts/, lib/）
- [ ] 验证 API 域名和 SSL 证书
- [ ] 准备测试凭证（非生产环境）
- [ ] 确认环境变量名称一致（EZVIZ_TEXT_CONTENT）

**安装时**:
- [ ] 设置 `EZVIZ_TOKEN_CACHE=0`（共享系统/多用户环境）
- [ ] 使用专用 OS 用户账户
- [ ] 在隔离环境运行（容器/VM）
- [ ] 使用环境变量而非 .env 文件

**安装后**:
- [ ] 验证缓存文件权限（600）
- [ ] 监控 API 调用日志
- [ ] 定期轮换凭证（90 天）
- [ ] 使用后清除缓存（高安全环境）
- [ ] 验证环境变量名称一致性

---

---

## 🔒 安全审计清单 (安装前完成)

根据安全审计建议，请在安装前完成以下检查：

### 安装前检查

- [ ] **审查代码** — 阅读 `scripts/audio_broadcast.py` 和 `lib/token_manager.py`
- [ ] **验证 API 域名** — 确认 `openai.ys7.com` 是萤石官方端点
- [ ] **准备测试凭证** — 创建专用萤石应用，仅开通语音相关权限
- [ ] **检查配置文件** — 审查 `~/.openclaw/*.json` 中是否有敏感凭证
- [ ] **确认缓存位置** — 确认 `/tmp/ezviz_global_token_cache/` 可接受

### 安装时配置

- [ ] **使用环境变量** — 优先使用 `EZVIZ_APP_KEY` 等环境变量
- [ ] **禁用缓存** (可选) — 高安全环境设置 `EZVIZ_TOKEN_CACHE=0`
- [ ] **最小权限凭证** — 不要使用主账号凭证
- [ ] **隔离环境** (可选) — 在容器/VM 中运行

### 安装后验证

- [ ] **验证缓存权限** — 确认缓存文件权限为 600
- [ ] **测试功能** — 使用测试设备验证广播功能
- [ ] **监控日志** — 检查 API 调用是否正常
- [ ] **记录凭证** — 安全存储凭证信息（密钥管理器）

### 持续维护

- [ ] **定期轮换凭证** — 建议每 90 天轮换一次
- [ ] **审查依赖** — 定期检查 `requests` 等依赖的安全更新
- [ ] **清理缓存** — 高安全环境使用后清除缓存
- [ ] **监控异常** — 关注异常 API 调用或错误

---

**更新日志**:

| 日期 | 版本 | 变更 | 说明 |
|------|------|------|------|
| 2026-03-18 | 1.0.3 | 修复 metadata | 添加 `config.tokenCache` 配置说明 |
| 2026-03-18 | 1.0.3 | 明确缓存行为 | 默认启用缓存，支持 `EZVIZ_TOKEN_CACHE=0` 禁用 |
| 2026-03-18 | 1.0.2 | 添加输入验证 | `validate_text_input()` 和 `validate_device_serial()` |
| 2026-03-18 | 1.0.2 | 添加凭证来源警告 | 从配置文件读取时显示警告 |
| 2026-03-18 | 1.0.2 | 更新 metadata 格式 | 使用 YAML 格式，添加 warnings 字段 |
| 2026-03-18 | 1.0.1 | 修复 Token 缓存 bug | `use_cache=None` 改为 `use_cache=True` |
| 2026-03-18 | 1.0.1 | 添加安全审计清单 | 根据安全建议添加完整检查清单 |
| 2026-03-18 | 1.0.1 | 明确配置文件行为 | 说明 `~/.openclaw/*.json` 读取逻辑 |
| 2026-03-18 | 1.0.1 | 添加 API 域名验证 | 提供域名和 SSL 验证命令 |
| 2026-03-18 | 1.0.1 | 添加 TTS 安全说明 | 说明 subprocess 调用系统命令的安全措施 |

**最后更新**: 2026-03-18  
**版本**: 1.0.3 (缓存配置修复版)

---

## Channels 配置（推荐）

技能支持从 OpenClaw 的 channels 配置中自动读取萤石凭证，无需单独设置环境变量。

### 配置方式

在 `~/.openclaw/config.json` 或 `~/.openclaw/channels.json` 中添加：

```json
{
  "channels": {
    "ezviz": {
      "appId": "your_app_id",
      "appSecret": "your_app_secret",
      "domain": "https://openai.ys7.com",
      "enabled": true
    }
  }
}
```

### 配置搜索顺序

技能会按以下顺序查找配置文件：
1. `~/.openclaw/config.json`
2. `~/.openclaw/gateway/config.json`
3. `~/.openclaw/channels.json`

### 优先级

凭证获取优先级：
1. **环境变量** (最高优先级)
   - `EZVIZ_APP_KEY`
   - `EZVIZ_APP_SECRET`
2. **Channels 配置** (中等优先级)
   - `channels.ezviz.appId`
   - `channels.ezviz.appSecret`
3. **命令行参数** (最低优先级)

### 优势

- ✅ 集中管理凭证
- ✅ 无需每次设置环境变量
- ✅ 多个技能共享同一配置
- ✅ 更符合 OpenClaw 最佳实践
