Skill: wangshengli0421/aiznt-seedance (version 1.0.0)

--- name: aiznt-seedance description: > 即梦 Seedance 文生视频：豆包 Seedance 系列模型，content 数组格式。Use when: 用户指定 doubao-seedance 模型与多段文本/媒体内容。 metadata: openclaw: primaryEnv: TS_TOKEN requires: env: - AIZNT_PROXY_URLS --- # Seedance 视频 (aiznt-seedance) 本技能封装 **即梦 Seedance** 异步视频管线：向 [REDACTED_SECRET] 提交包含 `model` 与 `content` 数组的请求体，再用 [REDACTED_SECRET] 轮询任务状态。认证与天树其它代理技能相同，使用 **Bearer TS_TOKEN**。 ## 前置条件 - TsClaw **Skills** 中已为本技能配置 `TS_TOKEN` 与 `AIZNT_PROXY_URLS`（推荐一键「同步天树凭证」）。 - `AIZNT_PROXY_URLS` 解析后须包含： | 键名 | 说明 | |------|------| | [REDACTED_SECRET] | POST 创建任务 | | [REDACTED_SECRET] | GET 查询；URL 模板中 `{task_id}` 由脚本替换 | ## 请求体形状（概要） 上游通常要求： - `model`：如 `doubao-seedance-1-0-pro-250528`（以你环境可用模型名为准）。 - `content`：对象数组，元素含 `type`（如 `text`）与 `text` 等字段；具体嵌套规则以火山 / 豆包当前文档为准。 示例（仅作结构参考）： ```bash node scripts/seedance.js submit --body '{ "model": "doubao-seedance-1-0-pro-250528", "content": [ { "type": "text", "text": "A person walking through neon-lit alley, cinematic" } ] }' ``` 提交成功后，从响应中取出任务标识，再执行： ```bash node scripts/seedance.js fetch --task-id <任务ID> ``` ## 轮询建议 视频生成耗时较长，`fetch` 可能在多秒内返回 `processing` 类状态；由调用方（或 Agent）按间隔重复 `fetch`，直到成功、失败或超时。 ## 故障排查 - **URL 未替换占位符**：确认 [REDACTED_SECRET] 的值含字面量 `{task_id}`，且与脚本中 `expandUrl` 一致。 - **业务 code 非 0**：`client.js` 会将网关包装的错误信息抛出，请根据 message 调整模型名或配额。 ## 文件说明 - `scripts/seedance.js`：CLI（submit / fetch）。 - `scripts/client.js`：读取环境变量、拼 URL、`Authorization` 头、JSON 解析。

path: clawhub.json
content: {
  "name": "Seedance Video",
  "tagline": "即梦 Seedance 文生视频",
  "description": "Seedance video tasks.",
  "category": "media",
  "tags": ["seedance", "video", "tianshu"],
  "version": "1.0.0",
  "license": "MIT",
  "pricing": "free"
}


path: package.json
content: { "name": "aiznt-seedance-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,
};


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