Skill: lintqiu/focusavatar (version 0.1.0)

--- name: focusavatar description: 数字人生成 CLI 工具，凭 accessKeyId/accessKeySecret 调用后端 API，支持提交任务（MP3/MP4/文字→视频）与查询任务结果（orderNo）；不存储用户数据，凭证仅通过请求头发送。 category: media author: community keywords: focusavatar, 数字人, 数字人生成, AI数字人, 视频生成, ai视频, 语音合成, 唇形同步 --- # 数字人生成技能 (focusavatar) 命令行数字人生成工具，通过 **accessKeyId** 与 **accessKeySecret** 调用后端 API，完成「MP3 + MP4 模板 + 文字」到数字人视频的生成与结果查询。**安装或首次使用前请先前往控制台获取 accessKeyId 和 accessKeySecret**。 --- ## 目的与能力 - **目的**：在命令行或技能环境中快速调用数字人视频生成服务，无需在本地保存用户媒体文件。 - **能力**： - **提交任务**：输入 MP3 地址、MP4 地址（本地路径或 URL）及合成文字，提交到后端；支持轮询直至完成并输出视频链接。 - **查询结果**：凭任务单号 `orderNo` 查询任务状态与视频链接。 - **不落盘**：仅将地址与文本传给后端，不在本工具内存储。 - **交互引导**：分步输入、确认与重填，错误可追溯。 --- ## 凭证（accessKeyId / accessKeySecret） **使用前必读**：所有 API 调用均需认证，请先获取凭证： 1. **注册账号**：前往控制台（如 https://login.joycoreai.com/ 或部署方提供的地址）注册。 2. **购买/开通**：完成购买或开通流程。 3. **创建密钥**：在控制台创建密钥，复制 **accessKeyId** 和 **accessKeySecret**。 **使用方式**： - 运行脚本时按提示输入，或 - 设置环境变量免输入（将 `你的accessKeyId`、`你的accessKeySecret` 替换为控制台复制的真实值）： **Windows（PowerShell，当前窗口有效）**： ```powershell $env:FOCUSAVATAR_ACCESS_KEY_ID = "你的accessKeyId" $env:FOCUSAVATAR_ACCESS_KEY_SECRET = "你的accessKeySecret" ``` **Windows（CMD，当前窗口有效）**： ```cmd set FOCUSAVATAR_ACCESS_KEY_ID=你的accessKeyId set FOCUSAVATAR_ACCESS_KEY_SECRET=你的accessKeySecret ``` **Windows（永久）**： 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → 用户变量 → 新建 `FOCUSAVATAR_ACCESS_KEY_ID`、`FOCUSAVATAR_ACCESS_KEY_SECRET` 并填入对应值。 **Linux / macOS（当前终端有效）**： ```bash export FOCUSAVATAR_ACCESS_KEY_ID="你的accessKeyId" export FOCUSAVATAR_ACCESS_KEY_SECRET="你的accessKeySecret" ``` **Linux / macOS（永久）**： 将上述两行写入 `~/.bashrc` 或 `~/.zshrc`，再执行 `source ~/.bashrc` 或 `source ~/.zshrc`。 认证通过请求头 `X-Access-Key-Id`、`X-Access-Key-Secret` 传递；本技能不保存、不上传凭证到除后端以外的任何地方。 --- ## 指令范围（When to use） - 需要**生成数字人视频**（提供 MP3、MP4 地址与文字）。 - 需要**查询数字人任务结果**（已知 orderNo）。 - 需要调用 **focusavatar / 焦点数字人 / yunji 数字人** 相关 API。 - 在命令行或 OpenClaw 技能环境中操作数字人生成与查询。 --- ## 功能 - ✅ 使用前引导至指定地址获取 accessKeyId / accessKeySecret - ✅ **两种模式**：**提交任务**（走 focusavatar 原流程）/ **查询任务结果**（需提供 orderNo） - ✅ 分步引导输入：MP3 地址 → MP4 地址 → 文字内容 - ✅ 用户确认：可选择提交或重新输入 - ✅ 不下载文件：直接将地址传给后端处理，节省空间 - ✅ 进度显示：自动增长到 99% 后等待后端返回结果 - ✅ 轮询机制：异步任务自动轮询状态 - ✅ 一键输出：生成完成直接返回视频链接 - ✅ 任务完成后可凭 orderNo 通过「查询任务结果」再次查询 --- ## 后端接口 - **提交**：`POST {BASE_URL}/skill/api/submit` - Body：`mp3`, `mp4`, `text` - 返回：`videoUrl` 或 `orderNo`（需轮询） - **查询**：`POST {BASE_URL}/skill/api/api/result` - Body：`orderNo` - 返回：`status`、`progress`、`videoUrl`、`message` 等 BASE_URL 由环境变量 `FOCUSAVATAR_API` 指定（未设置时默认示例：`https://yunji.focus-jd.cn`）。 --- ## 安装 ```bash npx skills add https://github.com/lintqiu/focusavatar -s focusavatar -y 下载安装 ``` **首次安装完成后**：使用前请先前往指定地址获取 **accessKeyId** 与 **accessKeySecret**。 → 控制台地址：**https://login.joycoreai.com/** 或部署方提供的地址（注册 → 购买/开通 → 创建密钥）。 获取后可在使用技能时按提示输入，或设置环境变量 `FOCUSAVATAR_ACCESS_KEY_ID`、`FOCUSAVATAR_ACCESS_KEY_SECRET` 免输入。 安装后通过 OpenClaw 或技能入口启动本技能即可。 --- ## 体验操作流程（使用技能时） | 步骤 | 说明 | |------|------| | **① 设置凭证** | **自动**：已设置环境变量则无需输入。**一步一步**：未设置时按提示输入 accessKeyId、accessKeySecret。 | | **② 选择模式** | **两种模式**：[1] **提交任务**（走 focusavatar 原流程） / [2] **查询任务结果**（需提供 orderNo）。 | | **③ 设置 MP3 地址** | 选「提交任务」时，一步输入 MP3 路径或 URL。 | | **④ 设置 MP4 地址** | 一步输入 MP4 路径或 URL。 | | **⑤ 设置文字内容** | 一步输入需要合成的文字内容。 | | **⑥ 确认并提交** | 确认后提交；提交任务需等待约 5–10 分钟；查询任务输入 orderNo 即返回结果。 | 顺序：凭证（①）→ 模式（②）→ 若提交任务：MP3（③）→ MP4（④）→ 文字（⑤）→ 确认提交（⑥）；若查询则输入 orderNo。 --- ## 安装机制 **1. 通过 OpenClaw Skills 安装到本地** - **前置条件**：本机已安装 **Node.js**（含 npm），以便使用 `npx`。未安装可前往 [https://nodejs.org/](https://nodejs.org/) 下载安装。 - 在终端执行（将 `https://github.com/lintqiu/focusavatar` 替换为实际技能仓库地址，例如 `https://github.com/lintqiu/focusavatar`）： ```bash npx skills add https://github.com/lintqiu/focusavatar -s focusavatar -y 下载安装 ``` - **参数说明**：`-s focusavatar` 为技能名称，`-g` 表示按全局/规范安装，`-y` 表示默认确认、非交互。 - 安装完成后，技能会出现在 OpenClaw 的本地技能目录（如用户目录下的 `.openclaw/workspace/skills/focusavatar`，以 OpenClaw 实际约定为准）。 **2. 运行环境与依赖** - **Python**：需要 **Python 3.6 及以上**。 - 检查版本：`python3 --version` 或 `python --version`。 - 未安装请从 [https://www.python.org/downloads/](https://www.python.org/downloads/) 安装；Windows 安装时建议勾选「Add Python to PATH」。 - **requests**：必须安装，否则无法发起 HTTP 请求。 - Linux / macOS：`pip3 install requests` 或 `python3 -m pip install requests` - Windows：`pip install requests` 或 `py -m pip install requests` - 使用虚拟环境时，请在对应环境中执行上述命令。 **3. 权限与网络** - **无需系统级或 root/管理员权限**：以当前用户安装和运行即可。 - **需要网络访问**：会访问环境变量 `FOCUSAVATAR_API` 指定的后端（未设置时使用默认地址），请确保本机可访问该地址（防火墙、代理允许出站）。 --- ## 使用流程 1. **① 获取凭证**：首次使用前前往控制台获取 accessKeyId、accessKeySecret；使用时按提示输入或提前设置环境变量。 2. **② 选择模式**：[1] 提交任务（生成视频）/ [2] 查询任务结果（需 orderNo）。 3. **提交任务时**：**③ MP3 地址 → ④ MP4 地址 → ⑤ 文字内容** → 确认提交（⑥）；查询时输入 orderNo 即可。 详见上方「体验操作流程」。 --- ## 隐私说明 - 本工具仅作**客户端调用**，**不存储、不收集**用户的 MP3/MP4 地址、文字内容或生成视频。 - 所有请求**直接发往后端**（由 `FOCUSAVATAR_API` 指定），数据处理与存储由该后端及所属方负责。 - **凭证**：accessKeyId、accessKeySecret 仅用于构造请求头，不写入磁盘、不上传到本仓库或第三方。 - 用户需确保所提交内容符合法律法规，不侵犯第三方权益。 --- ## 坚持与特权 - **坚持（Persistence）**：不持久化用户业务数据；凭证可通过环境变量在会话间复用，由用户自行管理。 - **特权（Privileges）**：无需 root 或系统特权；需要网络权限；若由 OpenClaw/助手执行「提交任务」，建议设置较长超时（如 600 秒），因后端生成可能需数分钟。 --- ## When to use（摘要） - 需要生成数字人视频 - 需要查询数字人任务结果（orderNo） - 需要调用 focusavatar / 焦点数字人 相关 API - 命令行环境下快速生成或查询

path: README.md
content: # 数字人生成技能 (focusavatar)

命令行数字人生成工具，通过 **accessKeyId** 与 **accessKeySecret** 调用后端 API，输入 MP3 地址、MP4 地址和文字内容，生成数字人视频；支持提交任务与按 orderNo 查询结果。

---

## 目的与能力

- **目的**：在命令行或技能环境中，快速调用数字人视频生成服务，完成「音频 + 视频模板 + 文本」到合成视频的流程。
- **能力**：
  - 提交生成任务：传入 MP3/MP4 地址（本地路径或 URL）及合成文字，提交到后端并可选轮询结果。
  - 查询任务结果：凭任务单号 `orderNo` 查询状态与视频链接。
  - 不落盘处理：仅传递地址与文本给后端，不在本地存储用户媒体文件。
  - 交互式引导：分步输入、确认与重填，出错可追溯。

---

## 使用前必读：凭证（accessKeyId / accessKeySecret）

**安装完成后或首次使用前**，请先获取认证凭证：

1. **注册账号**：前往控制台（如 [https://login.joycoreai.com/](https://login.joycoreai.com/) 或部署方提供的地址）注册。
2. **购买/开通**：按需完成购买或开通流程。
3. **创建密钥**：在控制台创建密钥，复制 **accessKeyId** 和 **accessKeySecret**。

**使用方式**：

- **方式一**：运行脚本时按提示输入 accessKeyId、accessKeySecret。
- **方式二**：设置环境变量，免每次输入（将下面的 `你的accessKeyId`、`你的accessKeySecret` 替换为控制台复制的真实值）：

  **Windows（PowerShell，当前窗口有效）**：
  ```powershell
  $env:FOCUSAVATAR_ACCESS_KEY_ID = "你的accessKeyId"
  $env:FOCUSAVATAR_ACCESS_KEY_SECRET = "你的accessKeySecret"
  ```

  **Windows（CMD，当前窗口有效）**：
  ```cmd
  set FOCUSAVATAR_ACCESS_KEY_ID=你的accessKeyId
  set FOCUSAVATAR_ACCESS_KEY_SECRET=你的accessKeySecret
  ```

  **Windows（永久：用户环境变量）**：  
  设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → 用户变量 → 新建：变量名 `FOCUSAVATAR_ACCESS_KEY_ID`，变量值填 accessKeyId；再新建 `FOCUSAVATAR_ACCESS_KEY_SECRET`，变量值填 accessKeySecret。

  **Linux / macOS（当前终端有效）**：
  ```bash
  export FOCUSAVATAR_ACCESS_KEY_ID="你的accessKeyId"
  export FOCUSAVATAR_ACCESS_KEY_SECRET="你的accessKeySecret"
  ```

  **Linux / macOS（永久）**：  
 将上面两行 `export` 写入 `~/.bashrc` 或 `~/.zshrc`，然后执行 `source ~/.bashrc` 或 `source ~/.zshrc`。

请求认证通过请求头传递：`X-Access-Key-Id`、`X-Access-Key-Secret`。本工具不保存、不上传凭证到除后端 API 以外的任何地方。

---

## 功能特性

- ✅ 使用前引导至指定地址获取 accessKeyId / accessKeySecret
- ✅ **两种模式**：**提交任务**（走 focusavatar 原流程）/ **查询任务结果**（需提供 orderNo）
- ✅ 分步引导输入：MP3 地址 → MP4 地址 → 文字内容
- ✅ 用户确认：可选择提交或重新输入
- ✅ 不下载文件：直接将地址传给后端处理，节省空间
- ✅ 进度显示：自动增长到 99% 后等待后端返回结果
- ✅ 轮询机制：异步任务自动轮询状态
- ✅ 一键输出：生成完成直接返回视频链接
- ✅ 任务完成后可凭 orderNo 通过「查询任务结果」再次查询
- ⏱️ **需等待超时说明**：**提交任务（生成视频）** 需等待后端处理，通常约 **5–10 分钟**；若由 OpenClaw/助手等执行，建议将超时时间设为 **600 秒（10 分钟）** 以上，避免中途被中断。

---

## 搜索关键词

focusavatar, 数字人, 数字人生成, AI数字人, 视频生成, ai视频, 语音合成, 唇形同步

---

## 安装

通过 OpenClaw Skills 安装（示例，仓库地址以实际为准）：

```bash
npx skills add https://github.com/lintqiu/focusavatar -s focusavatar -y 下载安装
```

**首次安装完成后**：使用前请先前往指定地址获取 **accessKeyId** 与 **accessKeySecret**，否则无法调用接口。  
→ 控制台地址（注册 → 购买/开通 → 创建密钥）：**[https://login.joycoreai.com/](https://login.joycoreai.com/)** 或部署方提供的同等地址。  
获取后可在下方「体验操作流程」中按提示输入，或设置环境变量免输入。

安装后通过技能入口使用即可。

---

## 体验操作流程（使用技能时）

使用技能后，按以下步骤完成**一次完整体验**：

| 步骤 | 说明 |
|------|------|
| **① 设置凭证** | **自动**：若已设置环境变量 `FOCUSAVATAR_ACCESS_KEY_ID`、`FOCUSAVATAR_ACCESS_KEY_SECRET`，无需输入。<br>**一步一步**：未设置时，按提示依次输入 **accessKeyId**、**accessKeySecret**（从控制台复制）。 |
| **② 选择模式** | **两种模式**：<br>• **[1] 提交任务（生成视频）**：走 focusavatar 原流程，后续需输入 MP3、MP4、文字。<br>• **[2] 查询任务结果**：需提供任务单号 **orderNo**，仅查询状态与视频链接。 |
| **③ 设置 MP3 地址** | 仅在选择「提交任务」时出现。一步输入 **MP3 路径或 URL**。 |
| **④ 设置 MP4 地址** | 上一步完成后，一步输入 **MP4 路径或 URL**。 |
| **⑤ 设置文字内容** | 一步输入 **需要合成的文字内容**。 |
| **⑥ 确认并提交** | 确认 MP3、MP4、文字无误后选择「提交」；提交后等待生成（约 5–10 分钟），完成后输出视频链接。若选「查询任务结果」，输入 orderNo 后即返回状态与链接。 |

**简要顺序**：凭证（①）→ 选模式（②）→ 若提交任务则依次：MP3（③）→ MP4（④）→ 文字（⑤）→ 确认提交（⑥）；若查询则输入 orderNo 即可。

---

## 安装机制

**1. 通过 OpenClaw Skills 安装到本地**

- 前置条件：本机已安装 **Node.js**（含 npm），以便使用 `npx`。未安装可前往 [https://nodejs.org/](https://nodejs.org/) 下载安装。
- 在终端中执行（将 `https://github.com/lintqiu/focusavatar` 替换为实际技能仓库地址，例如 `https://github.com/lintqiu/focusavatar`）：
  ```bash
  npx skills add https://github.com/lintqiu/focusavatar -s focusavatar -y 下载安装
  ```
- 参数说明：`-s focusavatar` 为技能名称，`-g` 表示按全局/规范安装，`-y` 表示默认确认、非交互。
- 安装完成后，技能会出现在 OpenClaw 的本地技能目录（具体路径由 OpenClaw 约定，如用户目录下的 `.openclaw/workspace/skills/focusavatar`）。

**2. 运行环境与依赖**

- **Python**：需要 **Python 3.6 及以上**。  
  - 检查版本：终端执行 `python3 --version` 或 `python --version`。  
  - 未安装时请从 [https://www.python.org/downloads/](https://www.python.org/downloads/) 安装；Windows 安装时建议勾选「Add Python to PATH」。
- **requests**：必须安装该库，否则无法发起 HTTP 请求。  
  - Linux / macOS：`pip3 install requests` 或 `python3 -m pip install requests`  
  - Windows：`pip install requests` 或 `py -m pip install requests`  
  - 若使用虚拟环境，请在对应环境中执行上述命令。

**3. 权限与网络**

- **无需系统级或 root/管理员权限**：以当前登录用户安装和运行即可，不需要以管理员或 root 执行。
- **需要网络访问**：本技能会访问由环境变量 `FOCUSAVATAR_API` 指定的后端地址（未设置时使用默认地址）。请确保本机能够访问该地址（防火墙、代理等允许出站访问）。

---

## 使用

通过技能入口启动后，先完成 **① 设置凭证**（见上方「体验操作流程」），再 **② 选择模式**：

- **[1] 提交任务（生成视频）**：按顺序 **③ MP3 地址 → ④ MP4 地址 → ⑤ 文字内容**，确认后提交；支持轮询直至完成并输出视频链接。
- **[2] 查询任务结果**：输入任务单号 `orderNo`，即可获取状态与视频链接。

详细步骤见 **「体验操作流程（使用技能时）」** 小节。

---

## 后端 API 协议

**BASE_URL**：由环境变量 `FOCUSAVATAR_API` 指定，默认示例：`https://yunji.focus-jd.cn`。

**认证**：请求头  
`X-Access-Key-Id`、`X-Access-Key-Secret`。

**提交接口**

- `POST {BASE_URL}/skill/api/submit`
- Body (JSON)：`{ "mp3": "地址/URL", "mp4": "地址/URL", "text": "合成文字" }`
- 返回：直接完成时 `{"videoUrl": "..."}`；异步时 `{"orderNo": "任务单号"}`，需轮询查询接口。

**查询结果接口**

- `POST {BASE_URL}/skill/api/api/result`
- Body (JSON)：`{ "orderNo": "任务单号" }`
- 返回：JSON，含 `status`（如 done/error）、`progress`、`videoUrl`、`message` 等。

---

## 隐私说明

- **数据归属**：本工具仅作为客户端调用方，**不存储、不收集**用户的 MP3/MP4 地址、文字内容或生成的视频。
- **请求流向**：所有请求直接发往用户配置的后端服务（如 `FOCUSAVATAR_API`），数据处理与存储由该后端及所属方负责。
- **凭证**：accessKeyId、accessKeySecret 仅用于当次请求的请求头，不写入磁盘、不上传到本技能仓库或第三方。
- **合规**：用户需自行确保所提交内容符合当地法律法规，不侵犯第三方知识产权及其他权益。

---

## 坚持与特权

- **坚持（Persistence）**：本技能不持久化用户数据；凭证可通过环境变量在会话间复用，由用户自行管理。
- **特权（Privileges）**：无需 root 或系统级权限；需要网络访问以调用后端 API；若通过 OpenClaw/助手运行，建议对「提交任务」设置较长超时（如 600 秒），因生成可能需数分钟。

---

## 依赖

- Python 3.6+
- `requests`：`pip install requests`

---

## 许可证

MIT

path: scripts/focusavatar.py
content: #!/usr/bin/env python3
"""
数字人生成工具
- 使用前请前往指定网址获取 accessKeyId 和 accessKeySecret（见下方引导）
- 支持两种操作：提交任务（MP3/MP4/文字） / 查询任务结果（orderNo）

关于 OpenClaw 执行超时：
- 提交任务生成视频需要后端处理时间，较长，请在运行时告诉助手：
  "运行这个脚本，需要等待 5-10 分钟，请设置超时 600 秒"
- 助手会根据你说的时间设置执行超时，不会提前中断
"""
import requests
import time
import sys
import json
import os

# 获取密钥的引导地址（注册 → 购买 → 创建密钥，获取 accessKeyId 与 accessKeySecret）
CREDENTIALS_GET_URL = "https://yunji.focus-jd.cn"
API_ENDPOINT = os.environ.get("FOCUSAVATAR_API", "https://yunji.focus-jd.cn")

def prompt(message):
    print(f"\n{message}", end='')
    sys.stdout.flush()
    return input().strip()

def print_credentials_guide():
    """安装/启动时提示用户获取 accessKeyId 与 accessKeySecret（体验流程 ① 设置凭证）"""
    print("\n" + "=" * 50)
    print("  📌 ① 设置凭证：使用前请先获取 accessKeyId 和 accessKeySecret")
    print(f"  🔗 请前往：{CREDENTIALS_GET_URL}")
    print()
    print("  1️⃣  前往上述地址 注册 账号")
    print("  2️⃣  完成 购买/开通")
    print("  3️⃣  在控制台 创建密钥，复制 accessKeyId 和 accessKeySecret")
    print()
    print("  获取后请在下方输入，或设置环境变量免输入：")
    print("     FOCUSAVATAR_ACCESS_KEY_ID / FOCUSAVATAR_ACCESS_KEY_SECRET")
    print("=" * 50 + "\n")

def get_credentials():
    """从环境变量或交互获取 accessKeyId 和 accessKeySecret，返回 (access_key_id, access_key_secret)"""
    [REDACTED_SECRET]"FOCUSAVATAR_ACCESS_KEY_ID", "").strip()
    [REDACTED_SECRET]"FOCUSAVATAR_ACCESS_KEY_SECRET", "").strip()
    if access_key_id and access_key_secret:
        # 环境变量已设置，直接使用，不提示输入
        print(f"✓ 已从环境变量读取 accessKeyId")
        print(f"✓ 已从环境变量读取 accessKeySecret")
        print()
        return access_key_id, access_key_secret
    if not [REDACTED_SECRET] = prompt("请输入 accessKeyId（可设置环境变量 FOCUSAVATAR_ACCESS_KEY_ID）: ").strip()
    if not [REDACTED_SECRET] = prompt("请输入 accessKeySecret（可设置环境变量 FOCUSAVATAR_ACCESS_KEY_SECRET）: ").strip()
    return access_key_id, access_key_secret

def make_auth_headers(access_key_id: str, access_key_secret: str):
    """根据 accessKeyId 和 accessKeySecret 生成请求头"""
    headers = {"Content-Type": "application/json"}
    if [REDACTED_SECRET]"X-Access-Key-Id"] = access_key_id
    if [REDACTED_SECRET]"X-Access-Key-Secret"] = access_key_secret
    return headers

def run_submit_flow(access_key_id: str, access_key_secret: str):
    """提交任务流程：③ MP3 → ④ MP4 → ⑤ 文字 → 确认 → 提交 → 轮询结果"""
    print("\n  📌 接下来，请您一步一步操作\n")
    while True:
        mp3 = ""
        while not mp3:
            mp3 = prompt("③ 请输入 MP3 路径/URL: ")
            if not mp3:
                print("❌ 不能为空，请重新输入")

        mp4 = ""
        while not mp4:
            mp4 = prompt("④ 请输入 MP4 路径/URL: ")
            if not mp4:
                print("❌ 不能为空，请重新输入")

        text = ""
        while not text:
            text = prompt("⑤ 请输入需要合成的文字内容: ")
            if not text:
                print("❌ 不能为空，请重新输入")

        print("\n请确认信息：")
        print(f"  MP3: {mp3}")
        print(f"  MP4: {mp4}")
        print(f"  文字: {text}")
        print()

        confirm = ""
        while confirm not in ["1", "2"]:
            confirm = prompt("请选择：[1] 提交  [2] 重新输入\n选择: ")
            if confirm not in ["1", "2"]:
                print("❌ 请输入 1 或 2")

        if confirm == "2":
            print("\n🔄 重新开始...")
            continue

        print("\n🚀 正在提交...")
        headers = make_auth_headers(access_key_id, access_key_secret)
        try:
            payload = {"mp3": mp3, "mp4": mp4, "text": text}
            resp = requests.post(
                API_ENDPOINT.rstrip("/") + "/skill/api/submit",
                json=payload,
                headers=headers,
                timeout=999999,
            )
            resp.raise_for_status()
            result = resp.json()
        except Exception as e:
            err_msg = str(e)
            resp = getattr(e, "response", None)
            if resp is not None:
                code_error = resp.headers.get("codeError") or resp.headers.get("codeerror")
                try:
                    body = resp.json()
                    if isinstance(body, dict) and body.get("data") is not None:
                        err_msg = str(body["data"])
                    elif isinstance(body, dict):
                        err_msg = body.get("message") or body.get("msg") or body.get("error") or err_msg
                except Exception:
                    try:
                        err_msg = (resp.text or err_msg).strip() or err_msg
                    except Exception:
                        pass
                if code_error is not None:
                    err_msg = f"{err_msg} [codeError: {code_error}]" if err_msg else f"codeError: {code_error}"
            print(f"\n❌ 提交失败: {err_msg}")
            return

        if "videoUrl" in result:
            print("\n✅ 生成完成!")
            print(f"\n📺 视频链接: {result['videoUrl']}")
            print("\n💡 查询任务结果接口: POST " + API_ENDPOINT.rstrip("/") + "/skill/api/api/result ，需提供 orderNo。")
            return

        if "orderNo" not in result:
            print(f"\n📄 后端返回: {result}")
            return

        task_id = result["orderNo"]
        status_url = API_ENDPOINT.rstrip("/") + "/skill/api/api/result"
        print(f"\n⚙️  任务已提交，orderNo: {task_id}")
        print("📌 进度将实时显示（轮询后端）...\n")

        poll_count = 0
        while True:
            try:
                poll_count += 1
                r = requests.post(
                    status_url,
                    json={"orderNo": task_id},
                    headers=headers,
                    timeout=999999,
                )
                r.raise_for_status()
                # 处理返回：如果是字符串就解析，已经是 dict 直接用
                raw = r.json()
                if isinstance(raw, str):
                    data = json.loads(raw)
                else:
                    data = raw

                progress_val = data.get("progress")
                status_str = data.get("status", "")
                progress_display = None
                if progress_val is not None and progress_val != "":
                    try:
                        if isinstance(progress_val, (int, float)):
                            progress_display = min(int(progress_val), 100)
                        else:
                            progress_display = min(int(float(str(progress_val).strip())), 100)
                    except (TypeError, ValueError):
                        pass

                progress_str = f"{progress_display}%" if progress_display is not None else "--"
                print(f"  ⏳ 实时进度: {progress_str}  状态: {status_str or '处理中'}  [第 {poll_count} 次轮询]")
                sys.stdout.flush()

                # 检查是否完成或失败，任意一种情况都退出循环结束会话
                status = data.get("status")
                if status == "done" and "videoUrl" in data:
                    print("\n✅ 生成完成!")
                    print(f"\n📺 视频链接: {data['videoUrl']}")
                    return
                elif status == "error" or status == "fail":
                    print(f"\n❌ 生成失败: {data.get('message', '未知错误')}")
                    return
                else:
                    # 还在处理中，继续轮询
                    time.sleep(11)
            except Exception as e:
                print(f"  ⚠️ 轮询异常 (将重试): {e}")
                sys.stdout.flush()
                time.sleep(4)

def run_query_flow(access_key_id: str, access_key_secret: str):
    """查询任务结果流程：输入 orderNo → 调用 /skill/api/api/result"""
    try:
        from result import query_result
    except ImportError:
        # 若单独运行或路径不同，使用内联实现
        def query_result(order_no: str, key_id: str, key_secret: str):
            url = API_ENDPOINT.rstrip("/") + "/skill/api/api/result"
            h = make_auth_headers(key_id, key_secret)
            r = requests.post(url, json={"orderNo": order_no}, headers=h, timeout=30)
            r.raise_for_status()
            # 处理返回：如果是字符串就解析，已经是 dict 直接用
            raw = r.json()
            if isinstance(raw, str):
                return json.loads(raw)
            else:
                return raw

    print("  📌 查询任务结果：请输入任务单号 (orderNo)\n")
    order_no = prompt("请输入 orderNo: ").strip()
    if not order_no:
        print("❌ 任务单号不能为空")
        return
    try:
        data = query_result(order_no, access_key_id, access_key_secret)
    except Exception as e:
        print(f"❌ 查询失败: {e}")
        return

    status = data.get("status", "")
    if status == "done" and data.get("videoUrl"):
        print("✅ 任务已完成")
        print(f"📺 视频链接: {data['videoUrl']}")
    elif status == "error":
        print(f"❌ 任务失败: {data.get('message', '未知错误')}")
    else:
        progress = data.get("progress", "")
        print(f"⏳ 状态: {status}, 进度: {progress}")
        print("📄 完整返回:", json.dumps(data, ensure_ascii=False, indent=2))

def main():
    print_credentials_guide()
    access_key_id, [REDACTED_SECRET])

    print("\n" + "=" * 50)
    print("       数字人生成工具")
    print("=" * 50)
    print("  📌 提醒：使用前需要先前往 https://yunji.focus-jd.cn 获取 accessKeyId 和 accessKeySecret")
    print()
    print("  📌 ② 选择操作模式（两种模式）：")
    print("     [1] 提交任务（生成视频）— 走 focusavatar 原流程")
    print("     [2] 查询任务结果 — 需提供 orderNo")
    print()

    choice = ""
    while choice not in ["1", "2"]:
        choice = prompt("请选择 [1 或 2]: ")
        if choice not in ["1", "2"]:
            print("❌ 请输入 1 或 2")

    if choice == "1":
        run_submit_flow(access_key_id, access_key_secret)
    else:
        run_query_flow(access_key_id, access_key_secret)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n👋 已取消")


path: scripts/result.py
content: #!/usr/bin/env python3
"""
查询数字人任务结果
调用后端 POST /skill/api/api/result，根据 orderNo 查询任务状态与视频链接。
"""
import requests
import json
import sys
import os

API_ENDPOINT = os.environ.get("FOCUSAVATAR_API", "https://yunji.focus-jd.cn")
RESULT_URL = API_ENDPOINT.rstrip("/") + "/skill/api/api/result"


def get_credentials():
    """从环境变量或交互获取 accessKeyId 和 accessKeySecret"""
    [REDACTED_SECRET]"FOCUSAVATAR_ACCESS_KEY_ID", "").strip()
    [REDACTED_SECRET]"FOCUSAVATAR_ACCESS_KEY_SECRET", "").strip()
    if not [REDACTED_SECRET] = input("accessKeyId（可设置 FOCUSAVATAR_ACCESS_KEY_ID，直接回车跳过）: ").strip()
    if not [REDACTED_SECRET] = input("accessKeySecret（可设置 FOCUSAVATAR_ACCESS_KEY_SECRET，直接回车跳过）: ").strip()
    return access_key_id, access_key_secret


def _auth_headers(access_key_id: str, access_key_secret: str):
    """根据 accessKeyId 和 accessKeySecret 生成请求头"""
    headers = {"Content-Type": "application/json"}
    if [REDACTED_SECRET]"X-Access-Key-Id"] = access_key_id
    if [REDACTED_SECRET]"X-Access-Key-Secret"] = access_key_secret
    return headers


def query_result(order_no: str, access_key_id: str = "", access_key_secret: str = "") -> dict:
    """
    根据 orderNo 查询任务结果。
    :param order_no: 任务单号
    :param [REDACTED_SECRET]
    :param [REDACTED_SECRET]
    :return: 解析后的结果 dict
    """
    headers = _auth_headers(access_key_id, access_key_secret)
    payload = {"orderNo": order_no}
    r = requests.post(RESULT_URL, json=payload, headers=headers, timeout=30)
    r.raise_for_status()
    raw = r.json()
    return json.loads(raw) if isinstance(raw, str) else raw


def main():
    if len(sys.argv) > 1:
        order_no = sys.argv[1].strip()
    else:
        order_no = input("请输入任务单号 (orderNo): ").strip()
    if not order_no:
        print("❌ 任务单号不能为空")
        sys.exit(1)

    access_key_id, [REDACTED_SECRET])
    try:
        data = query_result(order_no, access_key_id, access_key_secret)
    except Exception as e:
        print(f"❌ 查询失败: {e}")
        sys.exit(1)

    status = data.get("status", "")
    if status == "done" and data.get("videoUrl"):
        print("✅ 任务已完成")
        print(f"📺 视频链接: {data['videoUrl']}")
    elif status == "error":
        print(f"❌ 任务失败: {data.get('message', '未知错误')}")
    else:
        progress = data.get("progress", "")
        print(f"⏳ 状态: {status}, 进度: {progress}")
        print("📄 完整返回:", json.dumps(data, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

path: skill.json
content: {
  "name": "focusavatar",
  "version": "0.0.5",
  "description": "数字人生成 CLI：凭 accessKeyId/accessKeySecret 调用后端 API，支持提交任务（MP3/MP4/文字→视频）与查询任务结果（orderNo）。不存储用户数据，凭证仅通过请求头发送至后端。",
  "type": "python",
  "script": "scripts/focusavatar.py",
  "parameters": {
    "message": {
      "type": "string",
      "description": "可选测试或说明消息"
    }
  },
  "triggers": [
    "focusavatar",
    "数字人",
    "数字人生成",
    "AI数字人",
    "视频生成",
    "ai视频",
    "语音合成",
    "唇形同步"
  ],
  "credentials": {
    "FOCUSAVATAR_ACCESS_KEY_ID": "accessKeyId，控制台创建密钥获得",
    "FOCUSAVATAR_ACCESS_KEY_SECRET": "accessKeySecret，控制台创建密钥获得"
  },
  "instructionScope": [
    "生成数字人视频（提交 MP3/MP4 地址与文字）",
    "查询数字人任务结果（凭 orderNo）",
    "调用 focusavatar / 焦点数字人 / yunji 数字人 相关 API"
  ],
  "enabled": true
}

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