@yichong6/canva-core
v0.1.0-alpha.6
Published
YiChong Canva 应用的公司后端 SDK:账户、额度、R2 上传、模型任务与文件工具;不依赖 React 和 @canva/*
Readme
@yichong6/canva-core
YiChong Canva 应用共用的浏览器 SDK,只封装公司自己的后端。应用创建一个客户端,账户、额度、R2 上传、文本审核与图像/视频任务共用同一份身份。
本包不依赖 React,也不依赖任何 @canva/* 包:
- 插入设计、读取选中内容、打开外链等 Canva 能力,应用直接调用 Canva 官方 SDK。
- 额度 footer、错误提示等界面组件放在应用自己的代码里;本包提供它们需要的数据和纯函数(
deriveQuota、formatQuotaCount、buildPaymentUrl、canShowPaymentLink、getUserFacingErrorMessage)。 - 登录需要的 Canva 用户令牌由应用传入
account.getCanvaUserToken。
安装与授权
公开 npm 发布完成后,已获 YiChong 使用授权的开发者可以直接安装,无需 npm 私有包读取 Token:
npm install --save-exact @yichong6/[email protected] --registry=https://registry.npmjs.org/当前仓库已完成 public / alpha 发布配置;该配置不代表版本已上传。版本是否可下载以 npm registry 为准。公开分发保留版权,license: UNLICENSED,具体见 版权说明。下载包不授予公司后端访问权限;每个应用仍需自己的 App ID、已登记渠道及运行时账户授权。浏览器中不得配置服务端密钥。
仓库内联调可以继续用 file:../../package/core。在 core 目录执行:
npm ci
npm run check:release
npm packcheck:release 依次执行 TypeScript 检查、干净 ESM/CJS 构建、测试和实际 tarball 的离线安装、入口及类型消费者验证。lint 当前是 TypeScript 检查的别名,不表示执行了 ESLint。prepack 和源码目录发布的 prepublishOnly 都运行完整门禁。
包内只分发构建产物、类型声明、README 和许可证;不带源码映射、测试、开发脚本或凭据配置。JavaScript 仍然可读,这不构成代码保密措施。直接发布 .tgz 不会重跑仓库中的检查脚本,必须使用已经完整验证的同一压缩包。正式发布前先完成真实渠道联调;发布流程见源码仓库 docs/Canva-npm公开发布.md。
应用里的基本流程
import { auth } from "@canva/user";
import { createCanvaClient } from "@yichong6/canva-core";
declare const file: File; // 用户在界面中选择的文件
declare function addImageToDesign(options: { url: string }): Promise<void>; // 应用自己的代码,内部直接用 @canva/asset 和 @canva/design
const client = createCanvaClient({
appId: "你的 Canva App ID",
appChannel: "后端已登记的产品渠道",
baseUrl: "https://api.imgkits.com",
account: { getCanvaUserToken: () => auth.getCanvaUserToken() },
getLocale: () => "zh-CN",
});
await client.account.login();
const uploaded = await client.uploads.uploadImage(file);
const job = await client.jobs.create({
protocol: "legacy",
owner: "openai",
name: "gpt-image-2",
input: { prompt: "一只猫", image_input: [uploaded.url] },
});
const result = await client.jobs.wait(job, {
pollIntervalMs: 3000,
timeoutMs: 180000,
onUpdate: (status) => console.log(status.status),
});
// output 的形状由模型协议决定。这里仅演示返回单张图片 URL 的模型。
if (typeof result.output !== "string") throw new Error("模型返回了意外的结果形状");
await addImageToDesign({ url: result.output });如结果 URL 是临时地址,先用 client.uploads.uploadFromUrl(url) 转成稳定 R2 地址,再交给应用用 Canva 官方 SDK 插入设计。不同模型的输入、输出与计价仍由应用和后端目录确定;SDK 不猜测模型参数或把所有输出当图片。
对供应商临时图片的网络读取,可以显式开启有限重试:
const saved = await client.uploads.uploadFromUrl(imageUrl, {
fileName: "cover.png",
acceptedTypes: ["image/png", "image/jpeg", "image/webp"],
download: { maxAttempts: 3, retryDelayMs: 750 },
timeoutMs: 120_000,
});maxAttempts 包含第一次,默认 1、范围 1–5;retryDelayMs 默认 750,范围 0–60000ms。只重试 GET 网络/正文读取错误和 HTTP 408、429、5xx;不重试 404、大小/类型校验、主动取消或总超时。失败正文从零重下。下载、间隔、签名和 PUT 共用外层总超时,但签名/PUT 本身不自动重试。独立 downloadFile 直接接收 maxAttempts、retryDelayMs。此选项不重建模型任务,也不重复消费。
文本内容审核
复用当前账户的业务 JWT,调用 client.moderation.check({ text, scene: "default" }, { signal, timeoutMs: 30_000 })。scene 可省略。返回 decision: "pass" | "review" | "block",以及 requestId、categories、flaggedCategories、policyVersion、model、durationMs。包只封装接口,是否继续生成由应用决定;封面应用仅 pass 继续。
公开入口为 ${baseUrl}/api/public/text-moderation/check,baseUrl 配网关根地址。无需新前端密钥或 Canva 权限,不用内部服务 Bearer。输入必须完整且同时不超过 1024 码点和 1024 UTF-8 字节;moderationTextLimits、measureModerationText(text) 可供页面校验。SDK 不截断、拆段或缓存通过结果。
失败抛 ApiError,保留 code/status/requestId/userMessage,并提供可选 retryable/retryAfterSeconds;取消为 AbortError。每次只请求一次,不自动重试,不调用生成或扣费。后端初始策略质量与真实部署待验收;该独立接口不代表生成端点已强制审核。完整合同见源码仓库 docs/Canva-文本审核接入.md。
任务协议
| protocol | 创建端点 | 查询端点 | 适用场景 |
| --- | --- | --- | --- |
| legacy | /replicate/createSpecifyPrediction | /replicate/getPrediction | 当前多数图像、视频应用;视频目录的 provider_requests 也走这里 |
| legacy-version | /replicate/createPrediction | /replicate/getPrediction | 需要 version hash 的存量模型 |
| catalog-image | /replicate/ai-image/create | /replicate/ai-image/status | 图像目录的 catalogModelId / mode / selection |
| catalog-video | /replicate/ai-video/create | /replicate/ai-video/status | 视频目录新意图接口 |
jobs.create() 只在后端返回任务 ID 时成功。jobs.wait() 轮询到首次成功立即停止;失败终态抛 ApiError。AbortSignal 只取消本地等待或请求,不代表后端任务已取消。创建请求不会自动重试,避免付费任务重复创建。
账户和额度由 usa 管理。SDK 的 quota.check(cost) 只是界面预检,成本由业务给出;后端仍须独立校验身份、权限和计费。浏览器包里不得放服务端密钥。
显式消费积分
已完成的任务可以调用:
const receipt = await client.quota.consume(1, {
taskId: `${job.protocol}:${job.id}`,
});
// receipt.refresh === "failed" 只表示余额读取失败,消费已确认。
// 可以单独调用 account.refresh();不要重新调用模型,也不要换 taskId 再扣一次。调用前必须登录,并按产品约定确认任务成功及结果可用。这里的 1 是示例产品单价,不是包的默认价格。taskId 是必填的本实例防重复标识,不会作为幂等字段发送给不支持它的 usa 旧端点。
| 账户/配置 | usa 操作 | | --- | --- | | 非订阅、server_credits 且 free_cnt 为 null | add-free-usage-cnt,增加免费已用次数 | | 非订阅、server_credits 数值余额,或旧 normal 模式 | reduce-free-cnt,余额减去本次用量;normal 保留负数试用偏移 | | 有订阅 | 优先 add-monthly-usage,不足部分 reduce-free-cnt | | 免费无限 | 不记账;订阅无限仍累计月度用量 |
消费请求使用当前渠道及闭包内的 passport,必须收到 HTTP 成功且 ok: 0,然后刷新账户。数量必须为正,余额/月度支持最多两位小数,免费已用量支持一位;拒绝会被服务端舍入掉的数量。
同实例、同用户、同 taskId 的并发或重复调用复用同一个 Promise,成本不同则拒绝。不同任务同时消费会返回 quota_consumption_pending。失败返回 QuotaConsumptionError,区分明确拒绝、结果未知和拆分后的部分成功;confirmedCost 只表示已经明确成功的部分。失败不自动重试,账户预检会暂停新消费,普通余额刷新不能证明某个任务已记账,也不会清除这个状态。
**边界:**这不是服务端消费流水。实例重建、页面刷新、多个标签页不能靠内存锁去重。拆分消费也不是后端事务。正式上线前仍须由 usa/external_api 校验额度、任务归属,提供按任务只记账一次和状态查询能力;前端预检和任务 ID 不能替代这些校验。
目前 apps/demo 只接入账户和额度,尚未配置 baseUrl 或生成页面。接入新应用时应先用一个真实模型完成上传、创建、查询、结果插入 Canva 的预览验证。
