npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

yichong-canva2

v2.7.0

Published

使用一个 `YiChongCanvaProvider` 管理登录、订阅和额度,通过 `useYiChongCanva()` 直接调用后端接口。

Readme

yichong-canva2

使用一个 YiChongCanvaProvider 管理登录、订阅和额度,通过 useYiChongCanva() 直接调用后端接口。

2.7.0(2026-10-27):配合后端 msg 分级,新增 getDisplayMessage、getBackendErrorCode、isPolicyBlocked,ApiError 增加 errorCode、traceId、displayable;新通道按后端 error_code 推断错误码和是否预期内。已有字段含义不变。见下文「错误文案(2.7.0 起)」。另外新增 client.pricingUnit:计量单位按渠道和界面语言从定价语言包读取,见「计量单位(2.7.0 起)」。

2.6.0:新增行为与错误上报的新通道(schema canva.v1),默认关闭,升级后行为与 2.5.1 相同。开启后在旧 GET 统计、飞书错误上报之外额外发一份到 telemetry.imgkits.com;旧通道的请求内容、返回值和抛错行为不变。新增 client.track、reportUsage 的可选字段 flow / timings / error、use() 的可选第 4 个参数 { operationId },以及 StageError、toErrorCode、createStageTimer。见下文「新通道(2.6.0 起)」。

2.5.1:只补类型、不改行为。userStatus.profile 增加后端 status 已返回的 review?: boolean(审核开关:true 审核期、false 正常运行、缺失为旧账户)、canva_id?、created_at?、nextUsageResetTime?。支付链接是否可点由应用按 review === false || canAcceptPayments 决定,SDK 不据此改变任何行为。

2.5.0 已发布(2026-10-16):消费不再自动重试,use() 返回可核对的 UseResult;账户与审核缓存按身份隔离。新增显式轮询 waitForPrediction、取消请求 cancelPrediction、结果地址/取消识别工具,以及 uploadFile({ retention: "day30" })。可直接 npm install yichong-canva2@^2.5.0;修改 SDK 源码时仍用 npm pack 或脚手架 --sdk-path 验证。

2.4.0 新增:client.reportUsage 和 Provider 公共行为统计,兼容旧 GET 接口;另新增语音合成 client.createTextToSpeech、client.getTextToSpeechResult。下面的行为统计与 TTS 示例要求 2.4.0 或更高版本,2.3.0 不包含。

2.3.0 新增 client.createVtt 和 client.getVttStatus,统一调用音视频转录接口,并沿用 Provider 身份、通用参数、超时与取消支持。

2.2.0 新增账户请求超时、有限重试与自动飞书告警,公开 accountError 供 Footer 展示失败及重试;上报切换到最新指定的 webhook。

2.1.0 新增 onUploadProgress 和导出的 UploadProgress 类型,补齐视频目录参数,恢复消费数量原值透传,并将上传命名对齐旧随机算法与 MIME 后缀回退。上传继续复用 ai.imgkits.com 的四个 Cloudflare R2 接口,分片公开地址兼容旧响应,无需新增、迁移或部署后端接口。

使用

npm install yichong-canva2@^2.5.0

1.2.0 新增文本审核、三个消费接口,以及 Cloudflare R2 单次和分片上传;继续复用原有 Provider。

1.3.0 新增 client.reportError,直接调用现有错误上报 Worker,无需后端改动或额外配置。

1.3.1 补齐通用参数中的 timezone,并统一旧登录、通行证、状态查询、版本配置及旧消费请求。

1.4.0 新增 client.contentExtract 内容提取接口,已发布到 npm。

2.0.0 要求宿主应用使用 React / React DOM 19(^19.0.0),开发和测试依赖统一为 19.3.0,并新增 client.chat 大模型接口。仍使用 React 18 的旧应用请继续使用 1.x,升级 React 后再安装 2.x。

React 继续作为 peer dependency,不打入工具包产物。应用与 Provider 必须复用同一份 React;即使版本相同,本地源码链接也可能引入两个实例。开发时优先使用脚手架 --sdk-path 的 tarball 安装方式;直接链接源码时需将 React 解析统一到应用的依赖目录。

import { useState } from "react";
import { YiChongCanvaProvider, useYiChongCanva } from "yichong-canva2";

function App() {
  return (
    <YiChongCanvaProvider
      appId="YOUR_CANVA_APP_ID"
      productName="your_app_channel"
      baseUrl="https://usa.imgkits.com/your_app_channel"
      version={1}
    >
      <GenerateButton />
    </YiChongCanvaProvider>
  );
}

function GenerateButton() {
  const client = useYiChongCanva();
  const [error, setError] = useState("");
  const [status, setStatus] = useState("");
  const [busy, setBusy] = useState(false);
  const [prompt, setPrompt] = useState("a cat");

  async function generate() {
    setError("");
    setBusy(true);
    try {
      const moderation = await client.textCheck({ text: prompt });
      if (moderation.data.allowed !== true) {
        setError(moderation.msg);
        return;
      }
      const created = await client.createSpecifyPrediction({
        owner: "openai",
        name: "gpt-image-2",
        input: { prompt },
      });
      const prediction = await client.getPrediction({ id: created.data });
      setStatus(prediction.data.status);
      // getPrediction 只查询一次;仍在运行时由应用决定继续查询的时机。
      if (prediction.data.status === "failed") {
        setError(prediction.data.error_msg || prediction.data.error || "Generation failed");
      }
    } catch (error) {
      setError(error instanceof Error ? error.message : "Request failed");
    } finally {
      setBusy(false);
    }
  }

  return <>
    <textarea value={prompt} disabled={busy} onChange={event => setPrompt(event.target.value)} />
    <button disabled={client.loginStatus !== "success" || busy} onClick={() => void generate()}>Generate</button>
    {status && <p>{status}</p>}
    {error && <p role="alert">{error}</p>}
  </>;
}

baseUrl 沿用原有账户扣费接口的地址。预测、文本审核和音视频转录请求默认使用 https://api.imgkits.com,可以通过可选的 apiBaseUrl 指定测试地址。

文件签名服务默认使用 https://ai.imgkits.com,复用下表中的四个现有上传入口,可通过 Provider 的 uploadBaseUrl 指定测试服务。公开资源地址由后端响应的 endpointBaseUrl 提供:单次 PUT 使用普通签名响应;分片优先使用初始化响应,旧初始化响应缺少该字段时额外读取一次现有普通签名接口的地址信息,合并响应若提供最新地址则优先采用。uploadBaseUrl 是接口服务地址,不是公开资源地址。

内容提取默认使用 https://ai.imgkits.com,无需新增配置。Provider 可选的 contentBaseUrl 仅供覆盖测试服务地址;它独立于 apiBaseUrl 和 uploadBaseUrl,后者只控制上传签名服务。

大模型 chat 同样默认使用 https://ai.imgkits.com,无需新增配置。Provider 可选的 chatBaseUrl 仅供覆盖大模型测试服务地址,独立于 apiBaseUrl、contentBaseUrl 和 uploadBaseUrl。

语音合成 TTS 默认使用 https://ai.imgkits.com,Provider 的可选 ttsBaseUrl 可覆盖测试服务地址。它独立于预测与转录使用的 apiBaseUrl,正常使用不需要新增配置。

语言默认在每次请求时读取 document.documentElement.lang 或浏览器语言;应用有自己的国际化状态时,在 Provider 上传 getLocale={() => currentLocale}。

后端接口

| 调用 | 后端路径 | 成功响应 | | --- | --- | --- | | client.createSpecifyPrediction({ owner, name, input }) | POST /replicate/createSpecifyPrediction | { code: 0, msg: "ok", data: "任务 ID" } | | client.getPrediction({ id }) | POST /replicate/getPrediction | { code: 0, msg: "ok", data: { id, status, output, ... } } | | client.waitForPrediction({ id }, options?)(2.5.0) | 显式、有界地重复 getPrediction | Prediction,无 data 包装 | | client.cancelPrediction({ id }, options?)(2.5.0) | POST /replicate/cancelPrediction | 后端原样 Prediction 或 ApiResponse<Prediction> | | client.textCheck({ text, scene? }) | POST /api/public/text-moderation/check | { code: 0, msg: "展示文案", data: { allowed, decision, categories, ... } } | | client.contentExtract({ type, url }) | POST {contentBaseUrl}/api/content-extract | { code: 0, msg, data: { content: "提取的原文", ... } } | | client.chat({ payload, model?, img_url?, image_url?, image_urls? }) | POST {chatBaseUrl}/api/chat | { code: 0, msg, data: { result: "模型输出", ... } } | | client.createVtt({ audio?, video?, ifRender?, email?, env? }) | POST {apiBaseUrl}/get-vtt/create | { code: 0, msg, data: { jobId, status, status_str, remark } } | | client.getVttStatus({ jobId }) | POST {apiBaseUrl}/get-vtt/status | { code: 0, msg, data: { status, status_str, result, remark, final_video_url } } | | client.createTextToSpeech({ text, voice?, emotion?, speed?, language?, refer_audio?, refer_audio_text? })(2.4.0 起) | POST {ttsBaseUrl}/api/text-to-speech/create | { code: 0, msg, data: { jobId, status, status_str, remark } } | | client.getTextToSpeechResult({ jobId })(2.4.0 起) | POST {ttsBaseUrl}/api/text-to-speech/result | { code: 0, msg, data: { jobId, status, status_str, result, remark } } | | client.reportUsage({ sum_key, title, data? })(2.4.0 起) | GET https://log.aibabygenerator.io/api/report | Promise<boolean>;失败不抛错,不阻断业务 | | client.reportError({ error, source?, stage?, api?, context? }) | POST https://report.imgkits.com | Worker 原始 { success: true, data: { code: 0, ... } } 或过滤响应 | | client.track({ name, operation_id?, flow?, page?, props?, timings?, duration_ms?, error? })(2.6.0 起) | 新通道 POST https://telemetry.imgkits.com/v1/events(批量) | void;只在 telemetry.enabled 时发送,永不抛错 | | client.reduceFreeCnt({ cnt }) | POST {baseUrl}/api/user/reduce-free-cnt | { ok: 0, msg: "success" } | | client.addMonthlyUsage({ cnt }) | POST {baseUrl}/api/user/add-monthly-usage | { ok: 0, msg: "success" } | | client.addFreeUsageCnt({ cnt }) | POST {baseUrl}/api/user/add-free-usage-cnt | { ok: 0, msg: "success" } | | client.generatePresignedUrl({ objectName, bucketName? }) | POST {uploadBaseUrl}/api/generate-presigned-url | { success: true, url, endpointBaseUrl?, ... } | | client.initiateUpload({ objectName, bucketName?, contentType? }) | POST {uploadBaseUrl}/upload/initiate-upload | { UploadId, Bucket?, Key?, endpointBaseUrl?, ... } | | client.getUploadUrl({ objectName, uploadId, partNumber, bucketName? }) | POST {uploadBaseUrl}/upload/get-upload-url | { url, ... } | | client.completeUpload({ objectName, uploadId, parts, bucketName? }) | POST {uploadBaseUrl}/upload/complete-upload | { ETag, Bucket?, Key?, Location?, endpointBaseUrl?, ... } | | client.uploadFile({ file, onUploadProgress?, ... }) | 自动单次 PUT 或初始化 → 获取公开地址 → 分片签名 / PUT → 合并;从 2.1.0 支持进度回调 | { url: "公开资源地址", key, endpointBaseUrl } |

Provider 自动注入自有后端请求 query 中的 uid、platform_uid、platform=canva_addon、app_channel、timezone,以及请求头 Accept-Language。时区每次请求通过 Intl.DateTimeFormat().resolvedOptions().timeZone 读取,如 Asia/Taipei;运行环境不支持时使用 UTC。浏览器请求不额外发送 accept_language 参数。

这些通用信息覆盖预测、文本审核、内容提取、大模型 chat、音视频转录、语音合成 TTS、消费、上传签名和分片管理,以及原有登录、通行证、状态查询、版本配置和 use 消费请求。登录开始时不附带未知的用户 ID;拿到 Canva 用户 ID 后附带 platform_uid,拿到系统用户 ID 后再附带 uid,后续刷新和轮询读取最新语言与时区。调用方无需配置时区或在业务参数中重复填写通用字段。

预测、文本审核、内容提取和大模型 chat 还携带 Authorization: Bearer <jwt>、Channel: ai;具名消费接口使用 X-Access-Ticket、X-Access-Channel;签名接口使用 Channel: node-nauth。R2 PUT 原样使用签名 URL,只携带文件的 Content-Type;错误上报 Worker 使用独立协议,身份与渠道信息放在诊断消息中。创建请求中的 user_id 使用系统用户 ID(userStatus.profile.id)。公共审核只要求有效的业务 JWT;uid 和 platform_uid 在账户状态包含这些值时才附带,审核身份由后端验签确定。

创建接口保留普通的 owner/name/input 合同,也支持后端现有的 video_type、video_catalog、catalog_model_id、primary_provider、execution_chain、provider_requests 参数;这些视频路由字段按原名和 JSON 结构传递。当 video_catalog: "schema_v2" 且 provider_requests 非空时,可省略顶层 input,由各供应商请求携带输入;普通任务仍须传 input 对象。SDK 不改写 prompt、选择供应商或调整执行链。创建、查询和文本审核只发送一次请求,不自动轮询、重试、额外扣费或插入 Canva 设计。消费由应用显式调用具名方法。原有登录、额度和结账会话方法继续由同一个 hook 提供。

任务轮询、输出与取消(2.5.0 起)

getPrediction 仍只查询一次;业务需要统一轮询时显式调用以下方法,不增加另一层改名的 API 包装:

import { getPredictionOutputUrl, isAbortError } from "yichong-canva2";

const controller = new AbortController();
const { data: id } = await client.createSpecifyPrediction(
  { owner: "your-owner", name: "your-model", input: { prompt } },
  { signal: controller.signal },
);
try {
  const task = await client.waitForPrediction(
    { id },
    { intervalMs: 3_000, maxPolls: 60, timeoutMs: 180_000, signal: controller.signal },
  );
  const url = getPredictionOutputUrl(task.output);
  if (!url) throw new Error("任务已完成,但没有图片地址");
  // 校验和使用业务结果;消费、插入 Canva 由业务显式触发。
} catch (error) {
  if (!isAbortError(error)) throw error;
}

默认每 3 秒查询一次,最多 60 次,所有请求与间隔共用 180 秒预算。只有 succeeded 默认成功;failed/canceled/aborted 终态优先,即使附带 URL 也失败。旧 QR 背景这类明确允许先使用输出的场景可传 allowEarlyOutput: true。网络错误或单次请求超时最多连续容忍 3 次,每次计入次数;鉴权、HTTP、业务和协议错误直接结束。次数/总预算耗尽抛 prediction_timeout,失败终态抛 prediction_failed,主动中止抛 aborted。

getPredictionOutputUrl 从字符串、数组和 { url } 递归提取第一个 HTTP(S) 地址,无则返回 "";它不判断任务完成。isAbortError 只识别名为 AbortError 的错误或 SDK 的 aborted,不把其它 DOMException 误判为取消。

服务端取消需独立调用 await client.cancelPrediction({ id }),不要传已经 abort 的 signal。失败正常抛 ApiError;不同供应商不一定支持真正终止执行,成功响应也不保证停止上游计费。本地中止 waitForPrediction 不会自动发送取消请求。

社区模型兼容由后端 external_api 提交 0d29626f 提供,需部署后生效;SDK 仍使用 createSpecifyPrediction({ owner, name, input })。按 owner/name 使用最新模型版本,作者更新可能改变效果。参见仓库 docs/replicate-社区模型不能走createSpecifyPrediction.md。

消费接口

三个具名方法复用 Provider 登录流程已获得的 passport,只提交指定的数量,原样返回账户服务的 { ok: 0, msg, ... }。后端拒绝、非法响应、HTTP 失败、超时或取消会抛出 ApiError;error.code 保留后端的失败 ok 值。

| 方法 | 用途 | 后端记账精度 | | --- | --- | --- | | reduceFreeCnt | 扣减免费次数 / 积分余额 free_cnt | 四舍五入到两位小数 | | addMonthlyUsage | 累计订阅月用量 monthly_usage_cnt | 四舍五入到两位小数 | | addFreeUsageCnt | 累计服务端计量模式的 free_usage_cnt | 四舍五入到一位小数 |

cnt 必须是有限的正数,并在安全数值范围内。SDK 原样提交,不因超过一位/两位小数而拒绝,也不在前端舍入;例如 reduceFreeCnt({ cnt: 1.234 }) 发送 1.234,由后端按两位小数处理。应用按自己的计价规则、账户类型和剩余额度选择接口;方法不会自动选择套餐或拆分月额度与积分。下面是已确认本次消费应扣 free_cnt 时的组件示例:

function ConfirmUsageButton() {
  const client = useYiChongCanva();
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  async function confirmUsage() {
    if (busy) return;
    setBusy(true);
    setError("");
    try {
      await client.reduceFreeCnt({ cnt: 1 });
      await client.refreshUserStatus();
    } catch (error) {
      setError(error instanceof Error ? error.message : "消费未确认");
    } finally {
      setBusy(false);
    }
  }

  return <>
    <button disabled={busy || client.loginStatus !== "success"} onClick={() => void confirmUsage()}>
      确认消费
    </button>
    {error && <p role="alert">{error}</p>}
  </>;
}

月用量和服务端免费用量分别使用 await client.addMonthlyUsage({ cnt: 1 })、await client.addFreeUsageCnt({ cnt: 1 }),每次只调用业务所需的接口。确认后按需调用 refreshUserStatus() 更新显示;消费方法不会乐观修改余额,也不自动发起账户刷新。

默认消费超时为 30 秒,无自动重试。请求已发出后,超时、网络错误或取消不能证明服务端没有扣费;不要因此直接重放消费。后端未提供此接口的任务去重键,重复调用会再次记账。原有 use() 保留兼容,同一次消费不要同时调用 use() 和具名方法。

原有 use 的结果(2.5.0 起)

const result = await client.use(1) 返回 Promise<UseResult>,始终 resolve;已有不 await 的调用仍可用。根据最新账户状态分配月额度与积分,单个 Provider 内的调用串行处理;切换账户后,旧调用不会更新新账户。保留本地乐观余额更新,因此不能仅看余额判断服务端记账成功。

| result.status | 含义 | | --- | --- | | charged | 本次需要上传的消费已确认成功,charged: true | | skipped | test_mode、in_review_test、free_unlimited、not_upload 或 account_changed | | failed | 参数、账户未就绪或服务端明确拒绝,charged: false | | unknown | 网络、超时等造成记账结果未知,charged: null;不可直接重复消费 | | partial | 混合消费一部分成功,其余失败、未知或跳过;charged: true 只表示部分确认 |

confirmed.monthly / confirmed.freeCnt 给出已确认数量。force、notUpload、测试模式保留原分支语义。未知或部分成功时先保留生成结果,再核对账户;不要通过再次 use() 自动补扣。

2.4.0 的免费消费网络错误可能无限重试;2.5.0 完全取消自动重试。每个消费请求检查 HTTP 和业务 ok === 0,各有 30 秒时限;混合消费最多顺序执行两次,月额度失败不继续扣积分。积分请求复用既有消费 API 的 JSON { cnt } 与 X-Access-Channel,替代旧 use 的 query cnt。编排层对失败自动上报一次,不等待报告、不让告警失败影响业务。旧 use() 的月额度仍使用历史公共账户入口,显式 addMonthlyUsage 仍使用产品 baseUrl。

版本键使用 yichong_version_{appId};测试信息和测试余额按应用、产品、用户隔离,不再读取没有归属的旧全局键。版本查询失败和切换应用的迟到请求不会污染新账户。

Cloudflare R2 文件上传

2.5.0 新增 retention?: "temp" | "day30":默认 temp,client.uploadFile({ file, retention: "day30" }) 使用 day30_ 前缀,其余命名、32 位随机 ID、单次/分片上传流程不变。retention 和 objectName 互斥,同时传入报参数错误。该参数只决定对象名,不改变后端协议或资源域名。

2026-10-16 的 R2 截图记录中,temp_ 1 天后删除对象并中止未完成上传,day30_ 30 天后删除对象(未列出中止上传规则)。真正清理由对应桶的生命周期配置决定,SDK 不保证配置永不变;完整前缀表保存在仓库 docs/工具包与脚手架-修复优化清单.md 第 7 节。

2.1.0 增加分片公开地址兼容处理和 onUploadProgress,继续复用 ai.imgkits.com 的现有接口,无需新增或部署后端。这里只接入 Cloudflare R2;aliyun-fc 是签名服务的托管平台,与文件存储不同,本次不接入阿里云 OSS,也不改变现有托管平台。

uploadFile({ file }) 自动完成上传:小于 100 MiB 使用单次 PUT,达到 100 MiB 使用分片。调用方式和返回值相同;也可传 multipart: true 让小文件使用分片,省略或 false 时按大小自动选择。它支持非空的 File / Blob,默认生成 temp_{app_channel}_{YYYY-MM-DD}_{32位随机}.{后缀} 对象名:app_channel 对应 Provider 的 productName,日期使用浏览器本地日期,随机部分沿用旧应用的 randomString(32):使用 Math.random() 从固定字符集 ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678 中逐位取值。bucket 默认 video。显式传入 objectName 时使用指定对象名,不套用默认命名格式;可传 bucketName 指定 bucket,Blob 可通过 fileName 指定扩展名;未提供文件后缀时沿用旧模板的 MIME subtype 回退(例如 audio/wav → wav、video/webm → webm),均缺失才使用 bin。

单次上传先获取普通签名,再 PUT 文件,公开地址由签名响应的 endpointBaseUrl 生成。分片先调用 initiateUpload:响应已有 endpointBaseUrl 时直接使用;旧初始化响应未返回该字段时,用相同的对象名和 bucket 调用一次现有 generatePresignedUrl,只读取响应中的 endpointBaseUrl。这次额外请求是为了取得旧分片响应缺少的公开地址信息;返回的整文件签名 URL 不用于上传分片。取得并校验有效的公开 HTTPS 基础地址后,再逐片 getUploadUrl / PUT,最后 completeUpload。

合并响应若提供 endpointBaseUrl,则以它的最新地址为准;未提供时保留初始化或兼容请求取得的地址。返回的 endpointBaseUrl 是最终采用的地址,两种方式都按对象路径段编码生成 url,key 保留原对象名。初始化响应提供了非法地址,或兼容签名响应缺少有效地址时,抛出 invalid_response 且不开始 PUT;合并响应提供非法地址也会报错。SDK 不从 S3 Location 或签名 URL 的 host 推测公开地址,也没有客户端固定域名兜底。

前端不新增存储密钥;请求继续使用 Channel: node-nauth 和通用 query 参数。公开地址始终采用后端返回值,不要求本次增加后端配置或迁移接口。

将 onUploadProgress 放在第一个参数中,其参数是导出的 UploadProgress 类型:{ loaded: number, total?: number }。单位沿用旧工具:单次 PUT 使用浏览器 XHR 的真实上传字节数;分片在每片 PUT 成功并取得 ETag 后通知已完成片数和总片数。应用用 loaded / total 驱动比例,不把所有进度都标成字节;浏览器无法确定单次总量时省略 total,界面应显示不定进度。未传回调时单次 PUT 继续使用 fetch。

100% 只表示传输进度,不代表服务端响应或分片合并成功;仍需 await client.uploadFile(...) 后使用公开 URL。取消或失败后停止通知;回调抛错会终止上传并抛出 code="upload_progress_failed",回调中不应自行抛错作为普通状态更新。

普通上传(小于 100 MiB,且未强制分片):

sequenceDiagram
    participant C as 浏览器/工具包
    participant B as 原上传后端
    participant R as Cloudflare R2

    C->>B: generatePresignedUrl<br/>POST /api/generate-presigned-url
    B-->>C: PUT 签名地址 + endpointBaseUrl
    C->>R: 向签名地址 PUT 整个文件
    Note over C,R: 传入进度回调时,按上传字节数报告进度
    R-->>C: 上传成功
    Note over C: 返回公开 URL、key、endpointBaseUrl

分片上传(达到 100 MiB,或显式传入 multipart: true):

sequenceDiagram
    participant C as 浏览器/工具包
    participant B as 原上传后端
    participant R as Cloudflare R2

    C->>B: initiateUpload<br/>POST /upload/initiate-upload
    B-->>C: UploadId + 可选 endpointBaseUrl

    opt 初始化未返回公开域名
        C->>B: generatePresignedUrl
        B-->>C: 读取 endpointBaseUrl
        Note over C: 仅取得公开域名<br/>不使用这次整文件 PUT 签名上传
    end

    Note over C: 文件切片,通常每片 5 MiB
    loop 按顺序上传每一片
        C->>B: getUploadUrl<br/>POST /upload/get-upload-url
        B-->>C: 当前分片的 PUT 签名地址
        C->>R: PUT 当前分片
        R-->>C: 返回 ETag
        Note over C: 保存 ETag,回调完成片数/总片数
    end

    Note over C: 此时进度 100%,仍需等待合并
    C->>B: completeUpload<br/>POST /upload/complete-upload<br/>提交片号和 ETag 清单
    B->>R: 合并分片
    R-->>B: 合并成功
    B-->>C: ETag + 可选 endpointBaseUrl
    Note over C: 返回公开 URL、key、endpointBaseUrl

图中的原上传后端默认是 https://ai.imgkits.com。分片公开地址优先级为:合并响应提供的新地址 → 初始化响应地址 → 初始化缺失时从现有签名接口补取的地址;取得无效地址会报错,不猜测或硬编码域名。图示是成功流程,进度达到 100% 仍须等待 uploadFile 的 Promise 成功。

业务组件仍放在同一个 Provider 内,直接调用 client.uploadFile。下面根据比例展示进度,在 Promise 完成后才显示结果:

import { useEffect, useRef, useState } from "react";
import { useYiChongCanva, type UploadProgress } from "yichong-canva2";

function FileUploadPanel() {
  const client = useYiChongCanva();
  const [busy, setBusy] = useState(false);
  const [progress, setProgress] = useState<number>();
  const [url, setUrl] = useState("");
  const [error, setError] = useState("");

  const controllerRef = useRef<AbortController | null>(null);
  useEffect(() => () => {
    controllerRef.current?.abort();
    controllerRef.current = null;
  }, []);

  async function upload(file: File) {
    if (controllerRef.current) return;
    const controller = new AbortController();
    controllerRef.current = controller;
    setBusy(true);
    setProgress(undefined);
    setUrl("");
    setError("");
    try {
      const uploaded = await client.uploadFile(
        {
          file, // 大文件自动分片;小文件也可加 multipart: true。
          onUploadProgress: ({ loaded, total }: UploadProgress) => {
            if (controllerRef.current !== controller) return;
            setProgress(total && total > 0 ? Math.min(100, loaded / total * 100) : undefined);
          },
        },
        { signal: controller.signal },
      );
      if (controllerRef.current !== controller) return;
      setUrl(uploaded.url);
      // Promise 完成才表示上传成功;进度 100% 不能替代此确认。
    } catch (error) {
      if (controllerRef.current === controller) {
        setError(controller.signal.aborted ? "已取消上传" : error instanceof Error ? error.message : "上传失败");
      }
    } finally {
      if (controllerRef.current === controller) {
        controllerRef.current = null;
        setBusy(false);
      }
    }
  }

  return <>
    <input
      type="file"
      disabled={busy || client.loginStatus !== "success"}
      onChange={event => {
        const file = event.target.files?.[0];
        if (file) void upload(file);
      }}
    />
    <button disabled={!busy} onClick={() => controllerRef.current?.abort()}>取消上传</button>
    {busy && <progress max={100} value={progress} aria-label="上传进度" />}
    {busy && <p>{progress === undefined ? "正在上传…" : progress >= 100 ? "等待上传确认…" : `${Math.floor(progress)}%`}</p>}
    {url && <a href={url} target="_blank" rel="noreferrer">查看上传文件</a>}
    {error && <p role="alert">{error}</p>}
  </>;
}

generatePresignedUrl({ objectName, bucketName? }) 提供底层签名接口,返回后端原始结构;它本身不上传文件。一键单次上传必须取得合法的 endpointBaseUrl 才 PUT,避免把临时签名地址当成公开资源地址。旧响应仅返回签名 URL 时,底层方法仍可读取;一键分片在初始化响应缺少公开地址时也会调用该方法,只使用其 endpointBaseUrl。

上传的整个流程共用一次超时和取消信号。单次 PUT 默认 120 秒,分片默认 30 分钟;可在第二个参数传 { signal, timeoutMs } 调整。取消或账户、产品渠道变化后不再发起下一片或合并;合并请求已发出时,取消等待不能保证服务端没有完成。工具包不自动重试或续传。

分片顺序上传,默认每片 5 MiB,超大文件自动增大片大小以保持不超过 10000 片;最后一片可以更小。按 R2 当前限制 校验文件大小(最多 5 TiB − 5 GiB)和对象名(最多 1024 个 UTF-8 字节),不将整份文件读入内存。

浏览器必须能读取每片 PUT 的 ETag。R2 bucket 的 CORS 要允许应用来源、PUT 和 Content-Type,并设置 ExposeHeaders: ["ETag"],参见 Cloudflare CORS 文档。读取不到 ETag 时抛出 code="missing_etag",停止上传,不会合并。

三个底层方法 initiateUpload、getUploadUrl、completeUpload 保留后端原始响应,使用上表中的参数,默认每次超时 60 秒。为兼容历史直接调用,initiateUpload 和 completeUpload 的响应类型仍将 endpointBaseUrl 定义为可选字段;一键 uploadFile 会在旧初始化响应缺少该字段时通过现有签名接口补取,不需要后端新增字段才能上传。初始化结果的 UploadId 大写,下一步参数使用 uploadId;合并的 parts 必须按编号升序排列,每项使用后端要求的 { ETag, PartNumber },ETag 的引号原样保留。手动上传时由应用切片、PUT 和收集 ETag;一般业务直接使用 uploadFile 即可。

当前后端没有终止分片会话的接口,取消只停止客户端流程,已上传分片需由 bucket 的生命周期规则清理。R2 官方文档 说明默认会在 7 天后清理未完成会话,实际以当前 bucket 配置为准。

内容提取

client.contentExtract({ type, url }, options?) 从 1.4.0 提供,直接封装 POST https://ai.imgkits.com/api/content-extract,继续使用同一个 Provider 的用户 JWT 和通用参数。业务直接调用这个方法,无需再写只改名或转发参数的包装层。

| 输入 | type | url | | --- | --- | --- | | 文件 | "file" | 文件上传后的公开地址 | | 上传的视频 | "video" | 视频上传后的公开地址;只提取语音内容 | | 网页 / YouTube 链接 | "link" | 原始公开链接 |

纯文本直接使用用户输入的原文,无需调用提取接口。url 应为不含用户名或密码的公开 HTTP(S) 地址,最长 4096 字符;私网地址、DNS 解析及目标资源是否允许访问等由后端校验。不要传 File、Blob、blob: 或 data: 地址。

文件先上传,再把公开地址交给内容提取;两个步骤由业务显式组织,取消信号可以共用:

// 放在 Provider 内的组件事件或完整业务流程中;file 为用户选择的 File。
const controller = new AbortController();

try {
  const uploaded = await client.uploadFile(
    { file },
    { signal: controller.signal },
  );
  const result = await client.contentExtract(
    { type: "file", url: uploaded.url },
    { signal: controller.signal, timeoutMs: 360_000 },
  );

  setContent(result.data.content); // 保留原文;空字符串也是合法结果。
} catch (error) {
  setError(error instanceof Error ? error.message : "内容提取失败");
}

// 取消按钮调用 controller.abort(),只终止客户端等待。

上传视频时使用 type: "video",网页和 YouTube 链接使用 type: "link":

const video = await client.uploadFile({ file: videoFile });
const speech = await client.contentExtract({
  type: "video",
  url: video.url,
});

const page = await client.contentExtract({
  type: "link",
  url: "https://example.com/article",
});

const videoText = speech.data.content;
const articleText = page.data.content;

这是一次同步长请求,直接返回 { code: 0, msg, data: { content, ... } },不需要 getPrediction 或轮询。content 必须为字符串,允许空字符串,原文和附加响应字段均保留,不 trim、截断、摘要或翻译。工具包不自动上传文件、扣费或重试。

第二个参数支持 { signal, timeoutMs },默认超时 360_000 毫秒(6 分钟,包含读取响应正文)。取消或超时只终止客户端等待,不能保证后端已停止提取。非法参数、HTTP 失败、后端 code 非 0、响应格式错误、网络错误、超时或取消沿用 ApiError;后端 msg 保留在 error.message / error.userMessage,应用应展示原始错误信息。

大模型 chat

client.chat(params, options?) 直接封装 POST https://ai.imgkits.com/api/chat,继续使用同一个 Provider。从 2.0.0 提供;1.4.0 不包含此方法。

const response = await client.chat({
  payload: {
    messages: [
      { role: "system", content: "你是文案助手,回答简洁清楚。" },
      { role: "user", content: "为一本旅行相册写三个封面标题。" },
    ],
    temperature: 0.7,
    max_tokens: 1000,
  },
});

console.log(response.data.result);

| 参数 | 说明 | | --- | --- | | payload.messages | 必填 JSON 对象数组;文本对话使用 system / user / assistant,系统提示也放在这里 | | payload 的其他字段 | JSON 模型参数,例如 temperature、max_tokens、max_completion_tokens、response_format;后端选取已支持字段,不保证任意 OpenAI 字段都生效 | | model? | 可选字符串,SDK 原样传递且不填默认值;实际模型选择与回退由后端决定 | | img_url? / image_url? / image_urls? | 图片地址简写,每个字段支持字符串或字符串数组;使用其中一种即可 |

消息的 content 支持字符串,也支持标准 text / image_url 多模态数组,例如 [{ type: "text", text: "描述图片" }, { type: "image_url", image_url: { url: imageUrl } }]。也可传图片简写与 payload: { messages: [] },由后端补入用户消息。当前后端先尝试 Qwen,失败后可回退到 Replicate;填入 model 不保证指定模型一定被使用。

需要先提取内容再处理时,由业务显式组合两个方法:

const controller = new AbortController();

try {
  const extracted = await client.contentExtract(
    { type: "link", url: "https://example.com/article" },
    { signal: controller.signal },
  );
  const response = await client.chat(
    {
      payload: {
        messages: [
          { role: "system", content: "将用户提供的文章概括成三个要点。" },
          { role: "user", content: extracted.data.content },
        ],
      },
    },
    { signal: controller.signal, timeoutMs: 360_000 },
  );
  setSummary(response.data.result);
} catch (error) {
  setError(error instanceof Error ? error.message : "内容处理失败");
}

// 取消按钮调用 controller.abort()。

接口一次同步返回原始 { code: 0, msg, data: { result, ... } },文本在 data.result,附加响应字段保留。SDK 不清洗、截断或自动解析 JSON;若业务请求 JSON 输出,由业务自行解析和校验。它不自动调用审核、消费、轮询或重试,也不接入 /api/chat/create、/api/chat/result 或流式接口。

默认超时 360_000 毫秒(6 分钟,包含读取响应正文),是客户端等待预算;后端模型回退可能花费更久,可通过第二参数的 timeoutMs 调整。signal 只取消客户端等待,不保证停止后端生成。失败沿用 ApiError,后端 msg 保留在 error.message / error.userMessage;上例直接展示该信息。内容提取与 chat 各自计算超时,共用取消信号。

音视频转录

继续使用原来的 YiChongCanvaProvider,子组件中直接调用 client.createVtt()、client.getVttStatus()。创建参数沿用后端的 audio / video;正文 userId 自动使用 Provider 的系统用户 ID(profile.id),不是 Canva 的 userId。JWT、渠道、平台 ID、语言和时区也由当前 Provider 注入。

import { useState } from 'react';
import { ApiError, useYiChongCanva } from 'yichong-canva2';

// 此组件放在现有 Provider 下。mediaUrl 应是上传后、服务端可访问的音频地址。
export function TranscriptionPanel({ mediaUrl }: { mediaUrl: string }) {
  const client = useYiChongCanva();
  const [jobId, setJobId] = useState('');
  const [result, setResult] = useState('');
  const [message, setMessage] = useState('');
  const [busy, setBusy] = useState(false);

  async function run(action: 'create' | 'status') {
    setBusy(true);
    setMessage('');
    try {
      if (action === 'create') {
        setJobId('');
        setResult('');
        // 视频使用 { video: mediaUrl }。本地 File 先单独调用 client.uploadFile()。
        const { data } = await client.createVtt({ audio: mediaUrl });
        if (data.status === -1) throw new Error(data.remark || 'Transcription failed');
        setJobId(data.jobId);
        setMessage('Task ready. Check its status for subtitles.');
      } else {
        const { data } = await client.getVttStatus({ jobId });
        if (data.status === -1) throw new Error(data.remark || 'Transcription failed');
        if (data.status === 10) {
          setResult(data.result ?? ''); // 字幕片段的 JSON 原文;需要数组时自行 JSON.parse。
          setMessage('Completed');
        } else {
          setMessage('Transcription is still in progress.');
        }
      }
    } catch (error) {
      setMessage(error instanceof ApiError ? error.userMessage
        : error instanceof Error ? error.message : 'Request failed');
    } finally {
      setBusy(false);
    }
  }

  return <>
    <button disabled={busy || !mediaUrl || client.loginStatus !== 'success'}
      onClick={() => void run('create')}>Create transcription</button>
    <button disabled={busy || !jobId || client.loginStatus !== 'success'}
      onClick={() => void run('status')}>Check status</button>
    <p>{message}</p>
    <pre>{result}</pre>
  </>;
}

data.result 当前是字幕片段数组的 JSON 字符串,形如 [{"start_time":"00:00:00.000","end_time":"00:00:01.500","text":"你好"}],并非 WEBVTT 文件文本。SDK 原样返回,字幕文件导出由应用另行处理。

示例用手动查询展示两个原始接口,应用可以在业务流程里安排每 2–5 秒轮询,并设置总时限、在完成/失败/组件卸载时停止。SDK 本身只请求一次;任务状态 0 为排队、2 为处理中、10 为成功、-1 为失败。code=0 不代表转录已经成功。

可选参数 ifRender: 0 | 1、email、env 原样传给后端;env: 'pre' 不立即派发,普通使用应省略。建议只传一个媒体地址,两者都非空时后端优先 video,允许另一个字段为空字符串。上传、轮询和消费不由这两个方法自动执行,后端原有用量处理保持不变。

两个方法均默认超时 60 秒,可用第二参数 { timeoutMs, signal } 覆盖或取消等待。取消请求不会取消后台任务,也不会自动重新创建。

服务地址沿用 Provider 的 apiBaseUrl,默认 https://api.imgkits.com。这是客户端默认值,使用前需要后端部署 /get-vtt/* 并确认对应网关转发;这两个方法自 2.3.0 起提供。

语音合成 TTS(2.4.0 起)

继续使用同一个 YiChongCanvaProvider,组件直接调用 client.createTextToSpeech() 和 client.getTextToSpeechResult()。默认地址分别是 https://ai.imgkits.com/api/text-to-speech/create 与 /api/text-to-speech/result;需要测试服务时再设置 Provider 的 ttsBaseUrl。

SDK 要求已有系统用户 ID 和 Canva 平台用户 ID。创建正文中的 guid 自动取 userStatus.profile.id,不是随机任务 ID,也不是 Canva 用户 ID;调用方不能传 guid、userId、app_id 覆盖身份或计费参数。后端现有实现根据 guid 关联用户及用量,不能将它称为 JWT 身份验证;Provider 有有效 JWT 时才发送,缺失时省略 Authorization,不会发送 Bearer null。语言、渠道和通用 query 由 Provider 补齐。

import { useState } from 'react';
import { ApiError, useYiChongCanva } from 'yichong-canva2';

// 放在现有 Provider 下。示例手动查询,保留 jobId 以便继续查看同一个任务。
export function SpeechPanel() {
  const client = useYiChongCanva();
  const [text, setText] = useState('你好,欢迎使用语音合成。');
  const [jobId, setJobId] = useState('');
  const [audioUrl, setAudioUrl] = useState('');
  const [message, setMessage] = useState('');
  const [busy, setBusy] = useState(false);

  async function run(action: 'create' | 'result') {
    setBusy(true);
    setMessage('');
    try {
      if (action === 'create') {
        setJobId('');
        setAudioUrl('');
        const { data } = await client.createTextToSpeech({
          text,
          voice: 'Arrogant_Miss',
          emotion: 'auto',
          speed: 1,
        });
        if (data.status === -1) throw new Error(data.remark || 'Speech generation failed');
        setJobId(data.jobId);
        setMessage('Task created. Check the result when it is ready.');
      } else {
        const { data } = await client.getTextToSpeechResult({ jobId });
        if (data.status === -1) throw new Error(data.remark || 'Speech generation failed');
        if (data.status === 10) {
          if (!data.result) throw new Error('The completed task did not return audio');
          setAudioUrl(data.result); // 原始音频 URL,不是字幕 JSON,也不是音频文件二进制。
          setMessage('Completed');
        } else {
          setMessage('Speech generation is still waiting or processing.');
        }
      }
    } catch (error) {
      setMessage(error instanceof ApiError ? error.userMessage
        : error instanceof Error ? error.message : 'Request failed');
    } finally {
      setBusy(false);
    }
  }

  return <>
    <textarea value={text} disabled={busy} onChange={event => setText(event.target.value)} />
    <button disabled={busy || !text.trim() || client.loginStatus !== 'success'}
      onClick={() => void run('create')}>Create speech</button>
    <button disabled={busy || !jobId || client.loginStatus !== 'success'}
      onClick={() => void run('result')}>Check result</button>
    <p>{message}</p>
    {audioUrl && <audio controls src={audioUrl} />}
  </>;
}

text 必需、非空且最多 5000 个 Unicode 字符。voice、emotion、speed、language、refer_audio、refer_audio_text 可选,按后端字段名传递。speed 范围为 0.5–2.0,SDK 不自行舍入,省略时后端使用 1.0;未填的音色、情感和语言由后端取默认值。使用参考音频时,refer_audio 应是后端可访问的 HTTP(S) URL,参考文本最多 5000 个 Unicode 字符,后端仅在参考音频非空时使用;工具包不会自动上传音频或改写用户文本。

创建返回任务 jobId;查询保留 jobId/status/status_str/remark/result。状态为 0 等待、2 处理中、10 完成、-1 失败。code=0 不是生成完成,创建本身也可能返回 status=-1,因此创建和查询均要检查任务状态。result 是音频 URL 字符串或 null,直接用于播放器或后续导入流程,无需 JSON.parse。

默认每次请求超时 60 秒,第二参数支持 { timeoutMs, signal }。SDK 只发送一次请求,不自动重试、轮询、插入 Canva 或再扣一次额度;后端已有自己的 TTS 字符用量处理。需要轮询时,由业务设置总时限,并在完成、失败或组件卸载时停止。取消或超时只停止客户端等待,不保证后台任务取消或未扣费;尤其创建超时后不要直接重放创建请求。

文本审核

client.textCheck 封装后端的公共文本审核接口,复用现有 Provider,无需另外配置令牌。请求发送到用户 JWT 公共入口。

const result = await client.textCheck(
  { text: userInput, scene: "default" },
  { timeoutMs: 30_000 },
);

if (result.data.allowed === true) {
  // 可以继续后续操作;保留原始 decision,但不重复计算审核阈值。
} else {
  // 在应用的提示组件中展示 result.msg。
}

应用只在 data.allowed === true 时继续。后端当前统一 pass/review 放行、block 拒绝;三种审核结果都可以是 HTTP 200 / code=0,它只表示审核执行成功。msg、request_id、decision、分类分数、policy_version、model、duration_ms 和附加字段原样保留。工具包严格验证 allowed 等响应字段,缺失或类型错误时抛出 ApiError。

text 必须非空,并同时满足最多 1024 个 Unicode 字符、1024 个 UTF-8 字节,例如 1024 个 ASCII 字符、341 个常见汉字或 256 个四字节 emoji。原文包含的空白会保留,不截断或拆片。scene 可省略,目前仅支持 "default"。本地非法参数抛出 code="invalid_request",不会发送请求。

审核不会自动创建生成任务。上面的完整示例由应用显式先审核 prompt,再调用 createSpecifyPrediction;直接调用创建接口也不会隐式触发审核。

HTTP 失败、后端 code 非 0、网络错误、无效响应、超时和取消都会抛出导出的 ApiError。服务端返回的 msg 保留在 error.message / error.userMessage,应用应在错误组件中直接展示。code=0 且 data.status="failed" 表示任务执行失败,查询方法会保留任务信息,应用应处理 error_msg / error。

审核的技术错误也抛出 ApiError,不能当作审核通过。error.status / error.code 保留 HTTP 状态和后端 code;错误对象中的 error.data 保留后端 error_code、request_id、retryable、allowed 等字段。429 返回的 Retry-After 秒数保留在 error.retryAfter,由应用决定重试时机。

const controller = new AbortController();
const prediction = await client.getPrediction(
  { id: taskId },
  { signal: controller.signal, timeoutMs: 30_000 },
);

取消只终止当前 HTTP 等待,不会取消已经创建的后端任务或撤销已提交的消费。创建、查询、文本审核和底层签名的默认请求超时为 60 秒,包含读取响应正文的时间;内容提取和大模型 chat 为 360 秒,消费为 30 秒,单次 PUT 上传为 120 秒,分片上传整体为 30 分钟。

错误文案(2.7.0 起)

后端对 createSpecifyPrediction、getPrediction 做了 msg 分级:失败时的 msg、任务的 error_msg / error 都是按 Accept-Language 翻译好的用户文案,原文挪到 debug_msg / raw_*(约定见仓库 docs/前后端约定-错误文案msg分级.md)。界面取文案只用 getDisplayMessage,取不到再显示应用自己的翻译文案:

import { getDisplayMessage, isPolicyBlocked } from "yichong-canva2";

function describeFailure(error: unknown): string {
  return getDisplayMessage(error)          // 后端分级后的用户文案
    || (isPolicyBlocked(error) ? policyText // 内容违规:换图 / 换样式
      : fallbackText);                      // 应用自己的翻译兜底
}

| 字段 / 函数 | 含义 | | --- | --- | | ApiError.displayable | message 能原样展示时为 true。创建失败看响应是否带 debug_msg,任务失败看任务是否带 error_code;其它接口、SDK 本地错误恒为 false | | ApiError.errorCode | 后端 error_code(snake_case),逻辑判断只看它 | | ApiError.traceId | 后端 trace_id(创建接口),新通道放进 request_id | | getDisplayMessage(error) | 可展示的后端文案,没有返回 "";会顺着 StageError / Error.cause 找,最多 5 层 | | getBackendErrorCode(error) | 后端 error_code,没有返回 undefined | | isPolicyBlocked(error) | 后端判定内容违规(content_policy_violation),或 StageError 的 code 为 POLICY_BLOCKED |

  • 不再需要维护「SDK 本地错误码清单」,也不要再用正则匹配 E005。
  • 新通道:后端 content_policy_violation 记为 POLICY_BLOCKED(generate_blocked,不告警);image_unavailable、invalid_input、too_many_concurrent_tasks、image_preprocessing_failed、no_output 记为预期内。
  • code、userMessage 含义不变:创建失败的 code 仍是后端数字 1,任务失败仍是 prediction_failed。
  • 后端没打开分级时 displayable 一律为 false,应用显示自己的兜底文案。

计量单位(2.7.0 起)

client.pricingUnit 是定价语言包里的计量单位,和定价页用同一份文件:改了定价单位,用户下次打开应用就生效,不用前端发版。

const { pricingUnit } = useYiChongCanva();
// { unitNoun: "Quiz", unitNounPlural: "Quizze", locale: "de" },读取中或拿不到时为 null
const unitNoun = pricingUnit?.unitNoun ?? appConfig.unitConfig.unitNoun;
  • 地址:https://project.deletetweets.ai/yichong_pricing_language/<productName>/<语言>.json 的 config.unit / config.plural_unit。
  • 语言:取界面语言的主语言(zh-TW → zh,pt-BR → pt),没有语言包再退到 en;换语言时重新读取,期间保留上一份单位,换渠道时旧单位作废。
  • 每次打开都用 cache: "no-cache" 带 ETag 校验,没变时只回 304;单次 10 秒超时,不阻塞登录。
  • 404、断网、超时、格式不对(非字符串、超过 40 字符、含 <>{})都只得到 null,不抛错、不上报:部分渠道本来就没有定价语言包,应用退回 config.ts 的 unitConfig。
  • 复数沿用「1 用单数,其余用复数」;语言包没有 plural_unit 时复数等于单数。
  • 也导出了 loadPricingUnit(productName, locale, signal?),供不用 Provider 的场景使用。

账户故障处理(2.2.0)

继续使用一个 YiChongCanvaProvider,无需额外的上报 Provider 或 Footer 上报包装。账户模块会自动报告最终失败,业务 API 的错误仍在业务 catch 中按需显式调用 client.reportError。

  • Canva token、身份验证、canvaid-login、passport 和 status 每步最多 10 秒(含响应正文)。首次登录及其重试共享 30 秒预算;retry 仍决定额外尝试次数,但不能突破总预算。
  • 登录最终失败后 loginStatus 变为 error,Footer 停止圆圈并保留原 Connect account 重试按钮。client.manualLogin() 开始新一轮;取消旧请求和计时器,迟到响应不再更新状态。
  • client.refreshUserStatus() 最多两次尝试,共享 30 秒;失败仍返回旧快照以兼容已有调用,同时设置 client.accountError。未登录时只返回当前快照,不抢占登录。返回快照不等于刷新成功,也不能据此认定消费成功。
  • 支付轮询保留原快慢间隔和默认五分钟 TTL;连续三次网络/服务器故障提前结束,确定的响应/业务错误直接结束。健康的状态未变化、正常 TTL 或主动停止不告警;收尾补刷真实失败仍报告。补刷最多 10 秒并取消超时请求。
  • accountError 为只读诊断快照或 null,包含 operation(login/refresh/poll)、stage、code、可用的 HTTP status、安全诊断 message、阶段尝试数 attempts、耗时 elapsedMs 和不带查询参数的 api。应用应根据状态显示本地化文案,不直接展示技术诊断。开始重试或成功后清除。
  • Provider 按应用、接口、错误类型五分钟去重。自动报告不等待发送完成,发送失败不影响界面、不递归上报;卸载、主动取消、旧流程被替换都不告警。无需全局异常监听。
  • 上报复用 https://report.imgkits.com 的现有转发协议,目标采用最新指定的 webhook,并携带应用/工具包版本。SDK 只收集必要的诊断字段,不发送 JWT、passport、查询参数或原始服务端正文。完全断网时不能保证告警送达。

脚手架 Footer 只读取状态、展示新增失败提示和调用已有重试方法,不自行发送账户报告。正常七种额度文案、样式和布局保持原样。已有应用需要升级 SDK,并同步 Footer 的错误展示分支;更新脚手架不会自动改写旧应用。

错误上报

继续使用同一个 Provider,业务代码直接调用 client.reportError。error 接受 Error、ApiError、字符串或其他被抛出的值。Provider 自动补充产品渠道、应用 ID、应用版本及可用的用户 ID。工具包内置现有 Worker 地址和固定上报令牌,通过 Worker 原有 robot 字段指定最新约定的飞书 webhook(业务应用无需再配置),应用无需新增配置或修改后端。

固定令牌会随 npm 包和应用浏览器产物公开,不能视为保密凭据。此请求不向 Worker 发送用户 JWT 或 passport;这类值会从诊断正文中移除。上报可在 Provider 登录前调用,尚未获得的用户 ID 标为 unknown。

function GenerateButton() {
  const client = useYiChongCanva();
  const [message, setMessage] = useState("");
  const [busy, setBusy] = useState(false);

  async function generate() {
    if (busy || client.loginStatus !== "success") return;
    setBusy(true);
    setMessage("");
    try {
      const created = await client.createSpecifyPrediction({
        owner: "your-model-owner",
        name: "your-model-name",
        input: { prompt: "一只猫" },
      });
      setMessage(`已创建任务:${created.data}`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : "生成失败");
      // 单独处理上报失败;界面仍显示原本的业务错误。
      void client.reportError({
        error,
        source: "api",
        stage: "create_prediction",
        context: { model: "your-model-name" },
      }).catch(() => {});
    } finally {
      setBusy(false);
    }
  }

  return <>
    <button disabled={busy || client.loginStatus !== "success"} onClick={() => void generate()}>
      生成
    </button>
    {message && <p role="status">{message}</p>}
  </>;
}

source 默认 "ui",支持 ui/api/runtime/promise/react/upload/host_sdk;stage 是可选的业务阶段(最多 128 字符)。api 可填 { method, url, status, request_id, response_code },context 是小型诊断对象。ApiError 自动提取 code、status、data.request_id,不发送完整 data。message 最多 2000 字符,stack 最多 3000;context 截断长字符串、数量和深度,移除常见凭据及 base64 文件。整体内容过大时抛出 invalid_request。应用仍应只传诊断所需信息,例如模型名、任务 ID 和处理阶段,避免传原始用户输入或文件。

成功原样返回 Worker 的 { success: true, data, ... }。data.status="ignored" 表示旧 Worker 过滤了噪声,可能附带 reason/country;其他成功结果已经确认飞书业务码为 0。HTTP 成功但飞书业务码非 0 仍抛出 ApiError;上报的错误码为 reporting_failed、invalid_response、network_error、timeout 或 aborted 等,保留 HTTP 状态及可读取的 Retry-After,不回显上游内部错误正文。不安装全局或 React 错误监听。账户登录、刷新和支付检查的最终失败由 Provider 自动上报;其它业务 API 的失败仍由业务显式调用。

默认超时 10 秒,支持第二参数 { signal, timeoutMs },只请求一次。沿用 Worker 的噪声过滤规则,本次不新增服务端限流或网关转发。上报失败不要再次上报自身异常;缺少 Provider 时抛出 provider_missing。apiBaseUrl 不影响上报地址,调用方不能覆盖机器人、地址或令牌。

开发

npm ci
npm run lint:types
npm run lint
npm test

npm test 构建后使用 Node 测试真实产物,覆盖请求合同、消费数量精度、签名与 PUT 的凭据边界、错误、超时、取消,以及 Provider 登录/刷新后的凭据共享。测试替换 Canva 宿主和网络请求,不调用真实模型、记账或上传资源。

源码在 packages/,构建输出在 dist/。npm run lint 检查新增 API 模块和测试;全包类型由 lint:types 检查。原包的旧 Provider 实现暂时保留。

用户行为统计(2.4.0 起)

保留原有一个 Provider 的用法,行为统计默认开启,不用额外包裹组件。Provider 自动记录 app_open、login_start、login_success、login_fail、account_fail 和 account_recovered;最后一项只有账户发生失败并真实恢复后才发出。普通成功的 status 轮询不逐次上报,避免高频噪声。client.reportUsage 的方法引用保持稳定,调用时读取当前用户身份。

<YiChongCanvaProvider
  appId="YOUR_CANVA_APP_ID"
  productName="your_app_channel"
  baseUrl="https://usa.imgkits.com/your_app_channel"
  version={1}
  usageReporting={{
    enabled: true, // 默认 true;false 同时关闭 Provider 公共事件和手动 reportUsage
    // appKey: "your_app_channel_canva", // 默认沿用旧应用 {productName}_canva
    // endpoint: "https://your-report-service.example/api/report", // 可替换完整接口地址
  }}
>
  <App />
</YiChongCanvaProvider>

应用只补自己的关键行为,仍然直接调用同名 API:

const client = useYiChongCanva();
void client.reportUsage({
  sum_key: "generate_success",
  title: "生成成功",
  data: {
    operation_id: operationId,
    duration_ms: 12800,
    result_count: 1,
    model: "your_model",
  },
});

operationId 由业务或脚手架在一次生成开始时创建,串联开始、成功、失败、取消等事件;每次上报由 SDK 单独生成事件 UUID,网络重试复用这个 UUID。Provider 自动补入内存会话 session_id 和应用版本 app_version(即 Provider 的 version),业务不可覆盖同名字段。user_id 取当前 userStatus.profile.id,登录前省略,仍可上报登录故障。登录、消费、AI 请求不会等待行为统计。

新脚手架自动记录三段式页面进入、处理开始/成功/失败/取消/重试、插入设计和打开套餐页,业务不要重复记录同一公共事件。createSpecifyPrediction 返回任务 ID 只表示提交成功;只有最终生成结果已就绪才能报成功,不能在提交后提前报 generate_success。实际事件名见生成项目的 src/app.tsx、src/pages/result.tsx、src/components/footer.tsx 和 AGENTS.md。

接口与旧应用一致,字段包含 app_key、sum_key、title、user_id、data、timestamp、uuid,并继续补公共 query 与 Accept-Language。data 保留旧 JSON 双重 URL 编码供统计后台解码。Worker 使用服务端接收时间作为统计 timestamp;客户端发生时间保存在 data.occurred_at。不发送用户 JWT、passport 或 cookie,也不读取本地存储补身份。

返回值为 Promise<boolean>:收到 HTTP 成功确认时为 true;关闭、非法参数、身份 getter/序列化异常、取消、网络等失败均为 false,不向业务抛错、不显示错误、不递归告警。通常 void client.reportUsage(...) 即可。默认每次请求 4 秒,网络/超时/429/5xx 最多重试一次,其他 4xx 不重试;第二参数 { signal, timeoutMs } 可取消或缩短等待(1–4000 毫秒,每次)。

只传枚举、计数、耗时、错误码等少量属性,不传提示词、完整 input/output、文件、资源地址、token、stack 或原始异常。SDK 会脱敏敏感字段和值,跳过 getter/toJSON/类实例,最多 3 层、30 个字段、数组 10 项、字符串 256 字符;最终 URL 上限 8192 字符,超长时先移除尾部业务属性。脱敏无法识别任意改名的用户文本,应用仍需只提供必要统计数据。统计失败不影响 Footer 原样式、加载状态或文案。

新通道(2.6.0 起)

新通道按 schema canva.v1 把行为和错误放进同一个事件流,批量发到 https://telemetry.imgkits.com/v1/events。完整设计见仓库 docs/canva-telemetry/(01 事件格式、02 SDK 上报层)。

默认关闭。 不传 telemetry 或 enabled: false 时不发出任何新通道请求。旧通道(GET 统计、飞书错误上报)在开启前后完全相同。usageReporting={{ enabled: false }} 同时关闭两条通道。

<YiChongCanvaProvider
  appId="YOUR_CANVA_APP_ID"
  productName="your_app_channel"
  baseUrl="https://usa.imgkits.com/your_app_channel"
  version={1}
  telemetry={{ enabled: true }} // 默认 false;endpoint 默认 https://telemetry.imgkits.com/v1/events
>
  <App />
</YiChongCanvaProvider>

开启后:

  • reportUsage 和 reportError 的每次调用都会额外登记一条新通道事件(reportError 记为 app_error);generate_retry 不进新通道(由 generate_start 的 retry_of 表达);data.mode: "demo" 的事件记为 env: "dev"。
  • Provider 的账户事件(app_open、login_*、account_*)自动进入新通道;login_fail / account_fail 带 error(stage: "account")。
  • use(cnt, force, notUpload, { operationId }) 结束后自动发 consume_result(status、cnt;失败或未知时带 error)。不改变扣费行为。

新增的可选字段和方法:

import { StageError, createStageTimer, toErrorCode } from "yichong-canva2";

// 1. 记录每一步耗时;失败时抛 StageError,携带失败步骤和错误码
const timer = createStageTimer();
try {
  const uploaded = await timer.run("upload", () => client.uploadFile({ file }, { signal }));
  // ...
} catch (error) {
  throw new StageError(error, { stage: "upload", code: toErrorCode(error, "upload") });
}

// 2. reportUsage 新增 flow / timings / error,只进新通道
void client.reportUsage({
  sum_key: "generate_fail", title: "Generation failed",
  data: { operation_id: operationId, duration_ms },
  timings: timer.timings,
  error: { stage: "upload", cause: error }, // code 省略时由 SDK 推断;cause 为 StageError 时直接取它的 stage/code
});

// 3. 新事件和自定义事件用 track,只进新通道,同步返回、永不抛错
client.track({ name: "quota_blocked", props: { cost: 2, state: "insufficient" } });
  • 应用使用这些新字段或方法时,依赖须为 yichong-canva2@^2.6.0。旧版本的 reportUsage 收到未知字段会返回 false 且不发送。
  • stage 只能取词表:input_read、selection_read、upload、text_check、content_extract、chat、create_prediction、wait_prediction、cancel_prediction、tts、vtt、direct_api、parse_result、download_result、local_render、insert、consume、render、account、unknown。
  • 审核拦截请用错误码 POLICY_BLOCKED:generate_fail 带这个错误码时,新通道记为 generate_blocked。
  • props 只保留扁平的字符串、数字、布尔或 null,最多 20 个;键名命中 prompt、text、url、token、file_name 等敏感词,或值里含 URL、邮箱、JWT 时,整键丢弃。
  • env:页面运行在 localhost、测试模式(updateTestMode)或 mode: "demo" 时为 dev;审核期为 review;其余为 production。注意发布产物的 NODE_ENV 固定为 production,不能据此判断开发环境。
  • 发送:满 20 条或 5 秒发一批,页面隐藏和 Provider 卸载时用 keepalive 发送剩余事件;只对网络、超时、429、5xx 重试两次;服务端返回 503 或 pause_seconds 后本会话停止发送;每会话最多 500 条;同一错误 60 秒内合并为 repeat_count。不读写本地存储。