Skill: wangshengli0421/aiznt-sora2 (version 1.0.0)

--- name: aiznt-sora2 description: > Sora2 文生视频：通过天树代理提交生成任务并轮询结果。Use when: 用户指定 sora_video2、sora-2-pro 等模型生成短视频。 metadata: openclaw: primaryEnv: TS_TOKEN requires: env: - AIZNT_PROXY_URLS --- # Sora2 视频 (aiznt-sora2) 本技能在 **TsClaw / OpenClaw** 技能目录下提供两个 Node 入口：`submit` 创建异步任务，`fetch` 按任务 ID 查询状态与产物。所有请求经天树下发的代理 URL 转发，需使用与天树对话一致的 **Bearer** 凭证。 ## 前置条件 1. **TsClaw** 已登录天树账号，并在 **Skills** 页对本技能执行「同步天树凭证」或手动保存配置。 2. 环境变量（由应用写入 `skills.entries`，执行时映射为进程环境）： - `TS_TOKEN`：天树 `ts_` 前缀的对话令牌。 - `AIZNT_PROXY_URLS`：JSON 对象字符串，至少包含本技能用到的两个键（见下文）。键名需与后端 `getAizntProxyByTokenUrls` 返回字段一致。 ## 代理 URL 键 | 键名 | 用途 | |------|------| | `v2_videos_generations` | POST，提交文生视频请求体（prompt、model 等） | | `v2_videos_generations_fetch` | GET，查询任务；模板中可含 `{task_id}`，脚本会替换为实际 ID | 若 URL 模板含 `{task_id}`，**不要**手写死任务号；先 `submit` 取回 ID，再传给 `fetch`。 ## 命令示例 在项目根或技能目录下（保证 `node` 能加载 `scripts/`）： ```bash # 1. 提交任务（body 为上游要求的 JSON，示例字段仅供参考，以实际模型文档为准） node scripts/sora2.js submit --body '{"prompt":"A coffee cup on a wooden table, slow pan","model":"sora_video2"}' # 2. 假设上一步返回中包含任务 id（字段名依上游封装可能为 id / task_id），查询进度 node scripts/sora2.js fetch --task-id <上一步任务ID> ``` `submit` 成功返回的 JSON 结构取决于天树网关与上游封装；若只看到嵌套在 `data` 内的字段，请以实际响应为准解析 `task_id`。 ## 常见错误 - **401 / 缺少 TS_TOKEN**：未同步凭证或 `apiKey` 过期，请在 TsClaw 重新同步或更新天树登录。 - **缺少 AIZNT_PROXY_URLS 某键**：凭证接口未返回对应代理路径，需后端配置齐 `v2_videos_generations` 与 `v2_videos_generations_fetch`。 - **HTTP 4xx 于 submit**：请求体字段名、模型名与上游不一致；对照当前可用的 Sora2 模型列表调整 `model` 与 `prompt` 约束。 ## 与 TsClaw 的对应关系 - Skills 配置里 **`apiKey`** 对应环境变量 **`TS_TOKEN`**（`primaryEnv: TS_TOKEN`）。 - **`env.AIZNT_PROXY_URLS`** 为整段 JSON 字符串；本技能运行时只需上述两个键的非空值。 ## 维护说明 脚本使用 Node 18+ 全局 `fetch`，无额外 npm 依赖。修改代理行为时请同步更新 `scripts/client.js` 中的 `loadClient` / `fetchJson` 与本文档中的 URL 键表。

path: clawhub.json
content: {
  "name": "Sora2 Video",
  "tagline": "Sora2 文生视频",
  "description": "Sora2 video generation.",
  "category": "media",
  "tags": ["sora", "video", "tianshu"],
  "version": "1.0.0",
  "license": "MIT",
  "pricing": "free"
}


path: package.json
content: { "name": "aiznt-sora2-skill", "private": true, "dependencies": {} }


path: scripts/client.js
content: /** 从环境变量读取 TS_TOKEN 与 AIZNT_PROXY_URLS */
function loadClient() {
  const [REDACTED_SECRET] || '').trim();
  if (!token) {
    throw new Error('缺少 TS_TOKEN（天树对话凭证 ts_xxx，对应 skills.entries 的 apiKey）');
  }
  const raw = process.env.AIZNT_PROXY_URLS;
  if (!raw || !String(raw).trim()) {
    throw new Error('缺少 AIZNT_PROXY_URLS（JSON 字符串，与 GET /miniapp/ai/chat/credentials 返回的 aiznt_proxy_urls 一致）');
  }
  let urls;
  try {
    urls = typeof raw === 'string' ? JSON.parse(raw) : raw;
  } catch {
    throw new Error('AIZNT_PROXY_URLS 不是合法 JSON');
  }
  if (!urls || typeof urls !== 'object') {
    throw new Error('AIZNT_PROXY_URLS 必须是对象');
  }
  return { token, urls };
}

function expandUrl(template, replacements = {}) {
  if (!template || typeof template !== 'string') {
    throw new Error('URL 模板无效');
  }
  let u = template;
  for (const [k, v] of Object.entries(replacements)) {
    u = u.split(`{${k}}`).join(String(v));
  }
  if (u.includes('{')) {
    throw new Error(`URL 仍有未替换占位符: ${u}`);
  }
  return u;
}

function authHeaders(token, extra = {}) {
  return {
    Authorization: `Bearer ${token}`,
    ...extra,
  };
}

async function fetchJson(url, options = {}) {
  const res = await fetch(url, options);
  const text = await res.text();
  let data;
  try {
    data = text ? JSON.parse(text) : {};
  } catch {
    throw new Error(`非 JSON 响应 (${res.status}): ${text.slice(0, 200)}`);
  }
  if (!res.ok) {
    const msg = data?.message || data?.error?.message || text.slice(0, 200);
    throw new Error(`HTTP ${res.status}: ${msg}`);
  }
  if (data.code !== undefined && data.code !== 0) {
    throw new Error(data.message || `业务错误 code=${data.code}`);
  }
  return data.data !== undefined ? data.data : data;
}

module.exports = {
  loadClient,
  expandUrl,
  authHeaders,
  fetchJson,
};


path: scripts/sora2.js
content: #!/usr/bin/env node
const { loadClient, expandUrl, authHeaders, fetchJson } = require('./client.js');

function parseArgs(argv) {
  const args = argv.slice(2);
  const cmd = args[0];
  const rest = args.slice(1);
  const opts = { _: [] };
  for (let i = 0; i < rest.length; i++) {
    const a = rest[i];
    if (a.startsWith('--')) {
      const key = a.slice(2);
      const next = rest[i + 1];
      if (next !== undefined && !next.startsWith('--')) {
        opts[key] = next;
        i++;
      } else {
        opts[key] = true;
      }
    } else {
      opts._.push(a);
    }
  }
  return { cmd, opts };
}

function readJson(val, label) {
  if (!val) return undefined;
  try {
    return JSON.parse(val);
  } catch {
    throw new Error(`${label} 须为合法 JSON 字符串`);
  }
}

async function main() {
  const { cmd, opts } = parseArgs(process.argv);
  if (!cmd || cmd === 'help' || cmd === '-h') {
    console.log(`用法:
  submit --body '<json>'   # prompt、model 等
  fetch --task-id <id>`);
    process.exit(0);
  }
  const { token, urls } = loadClient();
  const bodyFromOpts = () => {
    if (opts['body-file']) {
      const fs = require('fs');
      return JSON.parse(fs.readFileSync(opts['body-file'], 'utf8'));
    }
    if (opts.body) return readJson(opts.body, '--body');
    throw new Error('需要 --body 或 --body-file');
  };

  if (cmd === 'submit') {
    const url = urls.v2_videos_generations;
    if (!url) throw new Error('AIZNT_PROXY_URLS 缺少 v2_videos_generations');
    const body = bodyFromOpts();
    const data = await fetchJson(url, {
      method: 'POST',
      headers: authHeaders(token, { 'Content-Type': 'application/json' }),
      body: JSON.stringify(body),
    });
    console.log(JSON.stringify(data, null, 2));
    return;
  }
  if (cmd === 'fetch') {
    const taskId = opts['task-id'];
    if (!taskId) throw new Error('需要 --task-id');
    const tpl = urls.v2_videos_generations_fetch;
    if (!tpl) throw new Error('AIZNT_PROXY_URLS 缺少 v2_videos_generations_fetch');
    const url = expandUrl(tpl, { task_id: taskId });
    const data = await fetchJson(url, { headers: authHeaders(token) });
    console.log(JSON.stringify(data, null, 2));
    return;
  }
  throw new Error(`未知命令: ${cmd}`);
}

main().catch((e) => {
  console.error('Error:', e.message);
  process.exit(1);
});


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