# 上传源素材

## 准备上传

本地 Agent 先计算文件的完整字节数、实际 MIME 与 SHA-256，再通过 POST /v1/uploads/prepare 或 assets.upload.prepare 提交 name、mime、size、checksum、idempotency_key。文件大小为 1 字节至 64 MiB。checksum 使用 64 位小写十六进制。

同一文件的一次上传始终沿用原 idempotency_key；元信息改变时返回 409 IDEMPOTENCY_CONFLICT。准备结果含 upload_id、十五分钟有效期，以及 upload.url、PUT 方法和所需请求头。X-Upload-Token 只能用于该文件，不是账户 API Key，不能用于读取资源或创建付费任务。不要把上传凭证写入日志或反馈。

## 上传与登记

使用返回的 URL 和请求头上传原始字节。上传接收不需要额外携带账户密钥；原账号、接入 Key、连接或登录会话失效时会拒绝。上传后状态为 uploaded，尚不能用于报价。

通过 POST /v1/uploads/{upload_id}/complete 或 assets.upload.complete 完成登记。服务端重新读取存储对象并核对大小与 SHA-256，只有 completed 返回的 asset_id 才能放入 input.asset_ids。并发或重复完成返回同一资源；已删除的资源不会因重试而重新创建。

下面的 Node.js 示例需要 JISU_API、JISU_KEY 和 JISU_UPLOAD_IDEMPOTENCY_KEY 环境变量，命令参数依次为本地文件路径和实际 MIME。一次上传中断后使用同一个幂等键重跑。

```javascript
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import { createHash } from "node:crypto";

const [file, mime] = process.argv.slice(2);
const { JISU_API, JISU_KEY, JISU_UPLOAD_IDEMPOTENCY_KEY } = process.env;
if (!file || !mime || !JISU_API || !JISU_KEY || !JISU_UPLOAD_IDEMPOTENCY_KEY) {
  throw new Error("Missing file, MIME or required environment variables");
}
const bytes = await readFile(file);
const headers = { Authorization: "Bearer " + JISU_KEY, "Content-Type": "application/json" };
async function json(response) {
  const body = await response.json();
  if (!response.ok) throw new Error(body.error?.code || "UPLOAD_FAILED");
  return body;
}
const prepared = await json(await fetch(JISU_API + "/v1/uploads/prepare", {
  method: "POST", headers,
  body: JSON.stringify({
    name: basename(file), mime, size: bytes.length,
    checksum: createHash("sha256").update(bytes).digest("hex"),
    idempotency_key: JISU_UPLOAD_IDEMPOTENCY_KEY,
  }),
}));
if (prepared.status !== "completed") {
  await json(await fetch(prepared.upload.url, {
    method: "PUT", headers: prepared.upload.headers, body: bytes,
  }));
}
const completed = await json(await fetch(
  JISU_API + "/v1/uploads/" + prepared.upload_id + "/complete",
  { method: "POST", headers, body: "{}" },
));
console.log(completed.asset_id);
```

## 中断与过期

GET /v1/uploads/{upload_id} 和 assets.upload.get 可查询状态，查询接口不返回上传凭证。prepared 表示等待完整文件，uploading 表示正在接收，uploaded 表示待校验登记，completed 表示已登记，expired 表示已过期。

409 UPLOAD_IN_PROGRESS 或 429 UPLOAD_BUSY 时稍后查询原上传，不新建文件。中断可重传完整文件；本版不支持分片或断点续传。SHA-256、大小或真实文件类型不匹配会拒绝，不能仅修改 Content-Type 伪装文件。文本须为 UTF-8，JSON 须可解析。文件签名识别不等于完整媒体解码或安全扫描。

存储内容丢失或损坏时，完成接口返回 UPLOAD_CONTENT_MISMATCH 并恢复 prepared，可在有效期内重传。超过十五分钟使用新幂等键重新准备；Worker 清理未登记的过期对象。每个账号最多八个未完成会话、两个同时上传，平台同时接收上限为四个；单次接收限时六十秒。

## 直接上传与远程导入

既有 POST /v1/uploads 仍接受原始字节，使用账户 Bearer 凭证、实际 Content-Type 和经过 URL 编码的 X-Filename，校验后直接返回资源 id。这个接口没有准备会话的幂等恢复能力。

POST /v1/uploads/import 或 assets.import 接受 HTTPS url 和 name。平台拒绝内网、非标准端口与超限下载，并校验实际内容类型。

## 稳定引用

任务通过 input.asset_ids 引用本人已登记资源；GET /v1/assets/{id} 或 assets.get 重新签发下载链接。远程 MCP 无法读取本机路径，原始文件仍由本地 Agent 上传。使用本地域名时，外部供应商无法下载参考素材；真实上游验收等待配置公网可达的资源地址。

## 预览与下载

资源查询返回 url（预览）、download_url（附件下载）和 download_name（文件名）。链接有效期为一小时，过期后用稳定资源 ID 重新查询；持有链接即可在有效期内读取文件，请勿公开敏感资源的链接。文件支持 HEAD、单段 bytes Range 与 If-Range，便于音视频拖动；不支持的范围返回 416。

控制台可预览并复制 UTF-8 文本和 JSON 结果。预览最多读取 128 KiB，截断时会提示；下载始终读取完整原文件。

## 删除资源与作品

DELETE /v1/assets/{id} 或 assets.delete 会立即禁止读取和引用该资源，并排队清理存储文件；重复删除同一资源不会重复创建清理任务。存储暂时不可用时 Worker 自动重试，运营资源详情显示清理状态。正在执行的任务引用该素材时返回 409 ASSET_IN_USE。

控制台删除作品会一并删除其输出文件，保留账单、资金记录和任务记录。进行中的作品不能删除，其他任务仍在引用输出文件时也不能删除。克隆音色使用独立的音色删除流程。备份中的历史副本遵循备份保留策略，线上删除不代表历史备份已经清除。
