# 上传文件：直传与 Func 内生成｜Creght AI 编程指南

> Creght 站点上传文件的两条路径：talizen/assets 的 CDN 签名直传，与 Func 内的 ctx.assets.upload。含返回值、20 MiB 上限、表字段写法与常见错误。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [在服务端调用外部 API 并管理缓存](/api/ssr-external-api-cache.md)
- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [JSON 表：定义、读写与查询](/api/func-json-tables.md)
- [上传文件：直传与 Func 内生成](/api/func-assets-upload.md)
- [超时配置与流式响应](/api/func-timeout-streaming.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)
- [使用集成接入支付宝支付](/api/func-alipay-integration.md)

登录与用户

- [注册时验证邮箱](/api/auth-verified-registration.md)
- [实现找回密码与修改密码](/api/auth-password-reset.md)
- [在 Func 里实现登录](/api/auth-func-login.md)
- [在 Func 里查询用户](/api/func-user-directory.md)

本页目录

- [先选对路径](#which-path)
- [浏览器里的文件 → 签名直传](#浏览器里的文件-签名直传)
- [Func 里生成的文件 → ctx.assets.upload](#func-里生成的文件-ctx-assets-upload)
- [浏览器文件：CDN 签名直传](#browser-signed-upload)
- [Func 内生成的文件](#func-generated-upload)
- [把文件存进 JSON 表](#storing)
- [验收清单](#checklist)

后端/上传文件：直传与 Func 内生成

# 上传文件：直传与 Func 内生成

站点里的两条上传路径怎么选：浏览器选择的文件走 CDN 签名直传，Func 内生成的字节走 ctx.assets.upload（单次 20 MiB）。含为什么 base64 中转必然失败，以及文件在 JSON 表里该怎么存。

复制 Markdown 链接

站点里的文件有两条上传路径，选错会直接撞上体积限制： **用户在浏览器里选的文件走 CDN 签名直传**， **Func 里生成的字节走 `ctx.assets.upload`**。两条路都返回一个可直接使用的 URL，表里存的永远是 URL，不是文件内容。

**智能体目标**

浏览器里的 `File`/ `Blob` 用 `uploadAsset()`；只有 Func 内部产生、浏览器拿不到的字节才用 `ctx.assets.upload()`。任何情况下都不要把文件编码成 base64 通过 `invoke()` 传输，也不要把 base64 存进 JSON 表。

## 先选对路径

### 浏览器里的文件 → 签名直传

用户选择或拖入的头像、附件、图片。文件字节由浏览器直接 PUT 到 CDN， **不经过 Func**，因此不受 Func 的执行超时和返回体积限制。

### Func 里生成的文件 → ctx.assets.upload

AI 生成的图片、服务端拼的 PDF、第三方接口取回的字节。这些内容浏览器本来就没有，只能从 Func 传出去，单次上限 **20 MiB**。

> 把浏览器文件编码成 base64 再 `invoke()` 给 Func 转发，是这里唯一真正的错误写法：base64 会让体积膨胀约三分之一，同时占满 Func 的入参、执行时间和返回体积三项预算，稍大的文件必然失败。

## 浏览器文件：CDN 签名直传

用户在网页中选择或拖入的 `File`/ `Blob` 使用 `talizen/assets`：

```typescript
import { uploadAsset } from 'talizen/assets'

const asset = await uploadAsset(file, {
  onFileUploadProcess(fileName, progress) {
    console.log(fileName, progress)
  },
})

// asset.fileUrl === asset.url
```

`uploadAsset()` 会依次调用 `POST /api/asset/file/preupload` 获取短期签名地址、由浏览器直接 `PUT` 文件字节到该 CDN 存储地址，再调用 `POST /api/asset/file/ack` 确认上传。文件字节不经过 Func；相同内容可按哈希复用已有对象。

接口接受 `File` 或 `Blob`。传入无文件名的 `Blob` 时使用 `uploadAsset(blob, { fileName: 'avatar.webp' })`。返回值包含 `{ fileUrl, url, fileName, mimeType, size, hash }`，其中两个 URL 字段相同。签名上传同时支持预览域名和已发布站点域名。

上传完成后，把返回的 URL 交给 Func 落库，Func 侧只处理字符串：

```typescript
import { invoke } from 'talizen/func'

const asset = await uploadAsset(file)
await invoke('profile.setAvatar', { url: asset.url, size: asset.size })
```

> Func 收到的是浏览器给的 URL，所以要当成 **不可信输入** 校验：确认它指向平台 CDN 域名，再落库。

## Func 内生成的文件

```typescript
const asset = ctx.assets.upload({
  filename: 'report.pdf',
  mimeType: 'application/pdf',
  base64: input.base64,
})
await ctx.db.insert('reports', {
  userId: ctx.auth.requireUser().id,
  url: asset.url,
  size: asset.size,
})
```

`ctx.assets.upload()` 仅用于 Func 内生成、无法从浏览器直接上传的字节。它同步返回 `{ fileUrl, url, size }`，其中两个 URL 字段相同。保存 URL 和大小即可；不要保存内部路径，也不要把大段 base64 存入表或作为 Func JSON 结果返回。 **单次上传上限为 20 MiB**。

典型场景是把第三方返回的字节转存到自己的 CDN，避免依赖对方的临时链接：

```typescript
export async function generate(input, ctx) {
  const user = ctx.auth.requireUser()

  const response = await fetch('https://api.example.com/v1/images', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.EXAMPLE_API_KEY },
    body: JSON.stringify({ prompt: input.prompt }),
  })
  if (!response.ok) {
    ctx.response.status(502)
    throw new Error('upstream request failed')
  }

  const { image_base64 } = await response.json()
  const asset = ctx.assets.upload({
    filename: 'generated.png',
    mimeType: 'image/png',
    base64: image_base64,
  })

  ctx.db.insert('generations', { userId: user.id, url: asset.url, prompt: input.prompt })
  return { url: asset.url }
}
```

这类调用通常远超默认 5 秒超时，调用端需要显式放宽，见 [超时配置与流式响应](/api/func-timeout-streaming.md)。

## 把文件存进 JSON 表

表里存 URL 和元数据，不存文件本身。对应的 schema 字段用字符串加 `format: "uri"` 描述—— **表 schema 不支持 `"format": "file"`**：

```json
{
  "url": { "type": "string", "format": "uri", "contentMediaType": "image/*" },
  "size": { "type": "number" },
  "fileName": { "type": "string" }
}
```

字段与查询规则见 [JSON 表：定义、读写与查询](/api/func-json-tables.md)。

## 验收清单

- 浏览器选择的文件走 `uploadAsset()`，没有经过 base64 和 `invoke()` 中转。
- `ctx.assets.upload()` 只用于 Func 内生成的字节，且单次不超过 20 MiB。
- 表里存的是 URL 和大小，没有 base64、没有内部存储路径。
- 浏览器传上来的 URL 在落库前校验过来源。
- 涉及生成类的长耗时调用，调用端设置了匹配的 `timeoutMs`。

**完成标准**

文件字节只走一条与其来源匹配的路径，Func 的入参和返回值里只有 URL 和元数据，预览域名与已发布域名上的上传都验证过。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

微信客服

## 链接

- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://creghtlib.site.creght.com)
- [动效库](/design/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [自己写代码 vs Creght](/compare/self-coding.md)
- [外包 vs 自己做](/compare/outsourcing.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
