接入文件服务和 TOS
ChatKit 负责附件交互,不替接入方保存文件。要让附件在刷新和历史会话中仍然可用,并让用户和模型安全读取私有文件,需要接入自己的文件服务。
为什么需要文件服务
附件功能页的默认示例不需要后端也能显示文件,因为它把小文件转成了 source.type: 'inline',文件字节就在当前页面的消息里。这适合体验上传、附件栏、内联混排和编辑,但示例刷新后消息会消失,也没有验证私有文件授权或模型读取。
只做本地演示、文件很小且不需要历史时,可以继续使用内联资源。实际应用出现以下任一需求时,应接入文件服务:
- 刷新页面或重新打开历史会话后,附件仍然可用。
- 文件放在私有 TOS、S3 或自己的存储中,不能使用永久公共 URL。
- 模型需要读取附件,而且模型所在网络与用户浏览器不同。
- 文件较大,不能把 base64 长期放进消息、数据库和每次模型请求。
- 需要登录鉴权、租户隔离、配额、审计、病毒扫描或删除回收。
文件服务解决的核心问题是:消息历史只保存稳定资源,真正读取文件时再换短期地址。ChatKit 不持有对象存储 AK/SK,也不限制存储厂商。
数据流
链路中有三种不同的数据,不要混用:
用户预览和模型读取必须分别鉴权。 不要把 Blob URL、base64 临时预览或过期签名写回稳定资源。
文件服务契约
一个最小文件服务包含五个步骤:
POST /uploads 只提交文件元数据,返回短期上传地址。浏览器把文件字节直接 PUT 到该地址,再调用完成接口;服务端核对对象后才返回稳定资源。
客户端接入
下面的客户端实现可同时用于本地 Node 服务、TOS、S3 或自有存储。只需让业务后端实现上面的接口,并修改 baseUrl:
推荐使用同源 baseUrl,让现有 Cookie 或网关处理登录态。跨域并使用 Cookie 时设置 credentials: 'include';使用令牌时通过 headers 每次读取最新值。AK/SK、STS 主密钥和 bucket 管理凭证都不能进入浏览器。
maxTotalSize 只用于界面提前提示,服务端仍需独立校验。附件栏与内联混排的行为见附件。
示例一:本地 Node 服务
下面的纯 Node 服务把对象和资源状态保存在进程内存中,无需数据库、TOS 账号或第三方依赖。它实现了申请上传、实际 PUT、完成确认、预览和模型读取,适合先在本地验证 ChatKit 接入。
把代码保存为 attachment-server.mjs 后启动:
默认监听 http://localhost:8787,客户端 baseUrl 配置为 http://localhost:8787/api/attachments。服务重启后内存数据会清空;这份代码只用于本地联调,不提供登录、租户隔离、持久化、病毒扫描或生产级签名。
仓库内还有完整可运行的 Node 文件系统示例:
打开 http://localhost:5180/?storage=local&pace=0。它会保存文件、会话和消息,可验证上传、预览、模型读取、编辑和刷新恢复;实现位于 examples/basic/src/local-storage/。
示例二:TOS 服务
安装火山引擎 TOS Node.js SDK:
下面这份服务端示例实现与本地服务相同的五组接口,但对象实际写入私有 TOS 桶。资源记录仍放在进程内存中,适合联调;生产环境应把资源 ID、对象 key、租户、会话、上传状态和引用关系写入数据库。
把代码保存为 tos-attachment-server.mjs,用服务端环境变量启动。下面的星号是占位符,不要把真实 AK/SK 写进代码、前端环境变量或版本库:
使用临时 STS 凭证时再配置 TOS_STS_TOKEN=***。自定义 TOS endpoint 时配置 TOS_ENDPOINT;未配置时按 tos-${TOS_REGION}.volces.com 生成。
环境变量从这些位置获取:
AK/SK、STS Token 和 bucket 配置都只放在服务端环境变量中,不要写入前端环境变量、源码或文档示例。 创建桶时保持私有读写,浏览器上传、用户预览和模型读取都通过服务端签发短期 URL。
客户端仍使用上一节的 attachment-client.ts,只需把 baseUrl 指向这项服务。私有桶还需要一条 CORS 规则,允许实际业务 Origin:
不要填写 * Origin,也不要把桶改成公共读。 浏览器直传只需要短期预签名 PUT;用户预览和模型读取都使用服务端按需签发的短期 GET。
TOS 默认桶域名会对预签名或匿名访问强制返回 Content-Disposition: attachment,不能直接用于浏览器在线预览,详见存储桶域名使用限制公告。使用默认域名时让浏览器客户端物化 Blob URL:
已绑定可在线预览的自定义域名,或由业务文件服务代理并返回 inline 响应时,保持 materializePreview: false,直接打开短期 URL。model-source 不受浏览器预览限制,仍应返回模型服务可访问的 TOS 签名 URL。
上线前改成生产实现
本地和 TOS 示例都把资源记录放在进程内存中。生产环境必须保留 HTTP 契约,并把资源状态、权限和对象生命周期接入业务数据库:
POST /uploads在数据库创建pending资源记录,以租户、会话和随机 ID 生成对象 key,再用服务端 TOS SDK 返回短期预签名 PUT URL。POST /uploads/:id/complete使用 TOSHEAD Object核对临时上传对象的字节数和 MIME,再复制到禁止覆盖的稳定 key;之后才把资源标记为ready并返回稳定resource.reference。旧预签名 PUT 即使仍在有效期内,也只能改写临时 key,不能替换已确认资源。GET /resources/:id/preview校验当前用户是否能读该资源,再签发短期 GET URL;不要把这个 URL 写入消息历史。POST /resources/:id/model-source单独签发模型读取地址。模型服务不在用户浏览器网络内时,该地址必须能被模型所在网络访问。- 删除会话、资源过期和用户主动删除时,由业务服务执行对象清理;ChatKit 不直接删除云端对象。
AK/SK、STS 和 bucket 配置必须留在服务端。 浏览器只获得当前操作所需的短期地址。TOS bucket 的 CORS 只允许业务 Origin、PUT/GET/HEAD 和签名要求的请求头。
上线检查
- 上传申请和完成确认都校验文件名、MIME、声明大小、实际对象大小、数量与总额度。
- 资源 ID 必须绑定租户和权限,不能仅凭可猜测 ID 返回预览 URL。
- 上传 URL、预览 URL 和模型读取 URL 分开签发并设置较短有效期。
- 历史只保存稳定的
{ id, name, mimeType, size, source },不要保存 Blob URL、base64 临时预览或过期签名。 resolvePreviewUrl和resolveForSend必须处理AbortSignal;地址过期后重新申请。- 生产文件服务要有资源引用计数或归属关系、过期上传回收、会话删除清理和定时垃圾回收,不能依赖浏览器生命周期。
- TOS 默认桶域名需要启用
materializePreview;可在线预览的自定义域名或业务代理可以直接返回短期签名 URL。