diff --git a/app/api/materials/upload/refresh/route.ts b/app/api/materials/upload/refresh/route.ts
new file mode 100644
index 0000000..f3e7005
--- /dev/null
+++ b/app/api/materials/upload/refresh/route.ts
@@ -0,0 +1,71 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { refreshUploadSession } from '@/lib/upload-sessions'
+
+// Configure route
+export const runtime = 'nodejs'
+export const dynamic = 'force-dynamic'
+
+/**
+ * 刷新上传会话API
+ * 用于会话过期后恢复上传,不清空已上传的分片记录
+ *
+ * POST /api/materials/upload/refresh
+ * Body: { uploadId: string }
+ *
+ * 返回:
+ * - success: true/false
+ * - uploadId: string
+ * - ttl: number (秒)
+ */
+export async function POST(req: NextRequest) {
+ console.log(`[RefreshSession] 收到会话刷新请求 at ${new Date().toISOString()}`)
+
+ try {
+ // 验证用户身份
+ const session = await getServerSession(authOptions)
+ if (!session) {
+ console.log(`[RefreshSession] 用户未登录`)
+ return NextResponse.json({ error: '请先登录' }, { status: 401 })
+ }
+
+ // 解析请求参数
+ const { uploadId } = await req.json()
+
+ if (!uploadId) {
+ console.log(`[RefreshSession] 缺少uploadId参数`)
+ return NextResponse.json(
+ { error: '缺少uploadId参数' },
+ { status: 400 }
+ )
+ }
+
+ console.log(`[RefreshSession] 尝试刷新会话: ${uploadId}, 用户: ${(session.user as any).id}`)
+
+ // 刷新会话TTL
+ const success = await refreshUploadSession(uploadId)
+
+ if (!success) {
+ console.log(`[RefreshSession] 会话不存在或已过期: ${uploadId}`)
+ return NextResponse.json(
+ { error: '会话不存在或已过期' },
+ { status: 404 }
+ )
+ }
+
+ console.log(`[RefreshSession] 会话刷新成功: ${uploadId}, TTL: 4小时`)
+
+ return NextResponse.json({
+ success: true,
+ uploadId,
+ ttl: 4 * 60 * 60 // 4小时(秒)
+ })
+ } catch (error) {
+ console.error('[RefreshSession] 刷新会话失败:', error)
+ return NextResponse.json(
+ { error: '刷新会话失败' },
+ { status: 500 }
+ )
+ }
+}
diff --git a/docs/TUS协议迁移方案.md b/docs/TUS协议迁移方案.md
new file mode 100644
index 0000000..14194b5
--- /dev/null
+++ b/docs/TUS协议迁移方案.md
@@ -0,0 +1,391 @@
+# TUS 协议迁移方案文档
+
+> **状态**:已规划,暂不实施
+> **原因**:优先采用方案A(优化当前方案),TUS作为长期备选方案
+
+---
+
+## 📋 文档概述
+
+本文档记录了 TUS 协议迁移的完整技术方案,作为未来系统升级的参考。
+
+**核心原则**:
+- ✅ 零新增环境变量(复用现有配置)
+- ✅ 零新增 Docker 依赖
+- ✅ 渐进式迁移(保留回滚方案)
+- ✅ 一次性成功,避免返工
+
+---
+
+## 🎯 TUS 协议简介
+
+### 什么是 TUS
+
+**TUS(Tus Upload System)** 是一个基于 HTTP 的**可断点续传上传协议**,专门设计用于解决大文件上传的可靠性问题。
+
+### 核心特性
+
+| 特性 | 说明 |
+|------|------|
+| **断点续传** | 上传中断后可从断点继续,无需重新上传 |
+| **开放标准** | RFC 协议规范,跨平台互操作性 |
+| **可扩展性** | 支持多种扩展(校验、过期、终止等) |
+| **标准化实现** | 官方提供多语言客户端和服务端库 |
+
+### 工作流程
+
+```
+1. POST /files
+ → 创建上传会话
+ → 返回: 201 Created
+ → Header: Location: /files/abc123
+
+2. PATCH /files/abc123
+ → Header: Upload-Offset: 0
+ → Header: Content-Type: application/offset+octet-stream
+ → Body: [前50MB数据]
+ → 返回: 204 No Content
+ → Header: Upload-Offset: 52428800 (50MB)
+
+3. 重复步骤2直到上传完成
+```
+
+---
+
+## 📊 方案对比分析
+
+### 方案A(已实施)vs 方案B(TUS)
+
+| 维度 | 方案A(优化当前方案) | 方案B(TUS协议) |
+|------|---------------------|----------------|
+| **开发周期** | ✅ 1天完成 | ⚠️ 5-7天 |
+| **测试周期** | ✅ 已验证 | ⚠️ 3-5天 |
+| **配置风险** | ✅ 零新增变量 | ⚠️ 需要5+个新变量 |
+| **兼容性** | ✅ 100%向后兼容 | ⚠️ 需客户端升级 |
+| **代码量** | ✅ 100行 | ⚠️ 需重写2000+行 |
+| **维护成本** | ✅ 低(熟悉代码) | ⚠️ 中(新协议) |
+| **标准化** | ❌ 自定义 | ✅ RFC标准 |
+| **跨平台** | ❌ 需自己实现客户端 | ✅ 官方库支持 |
+
+### 性能对比
+
+| 指标 | 方案A | 方案B |
+|------|-------|-------|
+| **分片大小** | 50MB | 可配置(推荐50MB) |
+| **并发上传** | 2-3并发 | 原生支持 |
+| **断点续传** | ✅ 自定义实现 | ✅ 协议级支持 |
+| **请求数(1GB)** | 20个 | 20个 |
+| **网络开销** | 低 | 低 |
+
+### 结论
+
+**短期(当前)**:采用方案A
+- ✅ 立即见效
+- ✅ 零风险
+- ✅ 已完成部署
+
+**长期(未来)**:评估是否迁移到TUS
+- 需要跨平台客户端支持时
+- 需要标准化协议时
+- 有充足开发和测试时间时
+
+---
+
+## 🚀 TUS 迁移方案(预留)
+
+### 技术选型
+
+#### 服务端:@tus/server
+
+```json
+{
+ "dependencies": {
+ "@tus/server": "^2.3.0",
+ "@tus/file-store": "^1.0.0"
+ }
+}
+```
+
+#### 客户端:tus-js-client
+
+```json
+{
+ "dependencies": {
+ "tus-js-client": "^3.0.0"
+ }
+}
+```
+
+### 零配置迁移策略
+
+#### 原则:复用现有配置
+
+**❌ 不推荐**:新增大量环境变量
+
+```env
+# 不推荐这样做!
+TUS_MAX_FILE_SIZE=107374182400
+TUS_UPLOAD_DIR=/app/uploads/tus
+TUS_LOCK_STORAGE=redis
+TUS_REDIS_PREFIX=tus:lock:
+TUS_LOCK_TIMEOUT=3600
+```
+
+**✅ 推荐**:复用现有配置
+
+```typescript
+// lib/tus-config.ts
+export const TUS_CONFIG = {
+ // 复用现有UPLOAD_DIR
+ uploadDir: process.env.UPLOAD_DIR || './uploads',
+
+ // 复用现有REDIS_URL
+ redisUrl: process.env.REDIS_URL,
+
+ // 固定配置(无需环境变量)
+ maxSize: 100 * 1024 * 1024 * 1024, // 100GB
+ chunkSize: 50 * 1024 * 1024, // 50MB
+}
+```
+
+### API 设计
+
+#### 路由结构
+
+```
+app/api/files/
+├── route.ts # TUS主端点
+└── [fileId]/
+ └── route.ts # TUS文件操作
+```
+
+#### 端点实现
+
+```typescript
+// app/api/files/route.ts
+import { Server } from '@tus/server'
+import { FileStore } from '@tus/file-store'
+
+// 创建TUS服务器实例(复用现有配置)
+const tusServer = new Server({
+ path: '/api/files',
+ datastore: new FileStore({
+ directory: process.env.UPLOAD_DIR || './uploads',
+ }),
+})
+
+// 导出处理器
+export const runtime = 'nodejs'
+export const dynamic = 'force-dynamic'
+
+export async function POST(req: Request) {
+ return tusServer.handle(req)
+}
+
+export async function OPTIONS(req: Request) {
+ return tusServer.handle(req)
+}
+```
+
+### 渐进式迁移方案
+
+#### 阶段1:双模式运行(1-2周)
+
+```typescript
+// components/UploadModal.tsx
+const USE_TUS = process.env.NEXT_PUBLIC_ENABLE_TUS === 'true'
+
+export function UploadModal() {
+ if (USE_TUS) {
+ return
+ }
+ return
+}
+```
+
+#### 阶段2:灰度发布(2-4周)
+
+```typescript
+// 10% 用户使用TUS
+const USE_TUS = Math.random() < 0.1
+
+// 监控错误率和性能
+// 逐步提升到50%、100%
+```
+
+#### 阶段3:全量切换(1-2月)
+
+- 所有用户使用TUS
+- 保留旧代码1个月作为回滚
+- 确认稳定后删除旧代码
+
+---
+
+## ⚠️ 风险预防
+
+### 避免Redis配置问题
+
+#### 历史教训
+
+```typescript
+// ❌ 错误:独立配置Redis连接
+const tusRedis = new Redis(process.env.TUS_REDIS_URL, {
+ // 新的配置,可能出错
+})
+
+// ✅ 正确:复用现有Redis实例
+import { redis } from '@/lib/upload-sessions-redis'
+
+export class TusDataStore {
+ constructor() {
+ this.redis = redis // 复用现有连接
+ }
+}
+```
+
+### 避免Docker配置问题
+
+#### ❌ 不推荐:修改Docker配置
+
+```yaml
+# docker-compose.yml
+services:
+ app:
+ environment:
+ # 新增5+个环境变量
+ - TUS_MAX_FILE_SIZE=107374182400
+ - TUS_UPLOAD_DIR=/app/uploads/tus
+ - TUS_LOCK_STORAGE=redis
+ # ...
+```
+
+#### ✅ 推荐:使用默认值
+
+```typescript
+// lib/tus-config.ts
+export const TUS_CONFIG = {
+ // 使用现有环境变量或默认值
+ uploadDir: process.env.UPLOAD_DIR || './uploads',
+
+ // 硬编码配置(无需环境变量)
+ maxSize: 100 * 1024 * 1024 * 1024, // 100GB
+ chunkSize: 50 * 1024 * 1024, // 50MB
+ expirationPeriodInMs: 24 * 60 * 60 * 1000, // 24小时
+}
+```
+
+### 错误处理与降级
+
+```typescript
+// lib/tus-server.ts
+export async function initTusServer() {
+ try {
+ const tusServer = new Server({
+ datastore: new FileStore({
+ directory: TUS_CONFIG.uploadDir,
+ }),
+ })
+
+ // 验证配置
+ await validateTusConfig()
+
+ return tusServer
+ } catch (error) {
+ console.error('[TUS] 初始化失败,降级到传统上传:', error)
+ return null // 降级到传统上传
+ }
+}
+```
+
+---
+
+## 📋 实施清单
+
+### 前置条件
+
+- [ ] 充足的开发时间(1-2周)
+- [ ] 充足的测试时间(1-2周)
+- [ ] 跨平台客户端需求明确
+- [ ] 团队完成TUS协议培训
+
+### 开发阶段
+
+- [ ] 安装依赖包(@tus/server, tus-js-client)
+- [ ] 实现TUS服务端(/api/files)
+- [ ] 实现TUS客户端组件
+- [ ] 实现降级机制
+
+### 测试阶段
+
+- [ ] 单元测试(TUS协议)
+- [ ] 集成测试(与现有系统)
+- [ ] 性能测试(对比方案A)
+- [ ] 压力测试(并发上传)
+- [ ] 兼容性测试(各浏览器)
+
+### 部署阶段
+
+- [ ] 影子环境部署
+- [ ] 10% 灰度发布
+- [ ] 50% 灰度发布
+- [ ] 100% 全量发布
+- [ ] 监控关键指标
+
+---
+
+## 🎯 触发条件
+
+**建议在以下情况下考虑迁移**:
+
+1. **需要跨平台客户端**
+ - iOS、Android、桌面应用等
+ - TUS有官方库支持
+
+2. **需要标准化协议**
+ - 与第三方系统集成
+ - 需要遵循RFC标准
+
+3. **有充足的开发时间**
+ - 当前系统稳定运行
+ - 有1-2个月的迁移窗口
+
+4. **方案A遇到瓶颈**
+ - 当前方案无法满足需求
+ - 需要TUS的特定特性
+
+---
+
+## 📚 参考资料
+
+### 官方资源
+
+- [TUS 协议官网](https://tus.io/)
+- [TUS 协议规范](https://tus.io/protocols/resumable-upload.html)
+- [TUS Node.js Server](https://github.com/tus/tus-node-server)
+- [TUS JavaScript Client](https://github.com/tus/tus-js-client)
+
+### NPM 包
+
+- `@tus/server`: 服务端实现
+- `@tus/file-store`: 文件系统存储
+- `@tus/s3-store`: AWS S3 存储
+- `tus-js-client`: 客户端库
+
+### 社区实践
+
+- [NextJS-TUS 示例项目](https://github.com/zacheryvaughn/NextJS-TUS)
+- [Designing a High-Load File Upload System with TUS](https://ductruong.com/en/blog/2024/05/designing-a-high-load-file-upload-system-with-the-tus-protocol/)
+
+---
+
+## 📝 版本历史
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| v1.0 | 2026-01-27 | 初始版本,基于方案A的研究分析 |
+
+---
+
+**最后更新**:2026-01-27
+**状态**:已规划,暂不实施
+**优先级**:低(方案A已满足需求)
\ No newline at end of file
diff --git a/lib/chunk-upload-config.ts b/lib/chunk-upload-config.ts
index 096317f..012f879 100644
--- a/lib/chunk-upload-config.ts
+++ b/lib/chunk-upload-config.ts
@@ -1,80 +1,62 @@
/**
* 分片上传配置
- * 根据网络环境自动选择最优分片大小
+ * 统一使用50MB大分片,提升上传效率
*
* 策略:
- * - 内网用户:20MB分片(千兆网络,速度快,充分利用带宽)
- * - 公网用户:10MB分片(平衡性能和稳定性,减少超时风险)
+ * - 统一使用50MB分片(内网公网一致)
+ * - 请求数减少80%(1GB文件从100个减少到20个)
+ * - TCP利用率更高(单个分片传输时间更长)
*
* 优势:
- * - 内网用户:请求数减少75%,上传速度提升40%+
- * - 公网用户:请求数减少50%,超时风险可控(19.6秒 < 60秒)
+ * - 减少网络开销(请求数少)
+ * - 提升传输效率(TCP慢启动影响更小)
+ * - 降低服务器压力(并发处理更少)
*/
import { getNetworkStatus, initializeNetwork } from './network'
/**
- * 获取最优分片大小(根据网络环境)
+ * 获取最优分片大小(统一50MB)
* @returns 分片大小(字节)
*/
export async function getOptimalChunkSize(): Promise {
- // 获取当前网络状态
- let network = getNetworkStatus()
+ // 统一使用50MB分片(内网公网一致)
+ // 优势:
+ // 1. 减少请求数(1GB文件从100个减少到20个)
+ // 2. TCP利用率更高(单个分片传输时间更长)
+ // 3. 降低网络开销
+ const chunkSize = 50 * 1024 * 1024 // 50MB
- // 如果还没检测过,先初始化网络检测
- if (!network.lastChecked) {
- try {
- network = await initializeNetwork()
- } catch (error) {
- console.warn('[ChunkConfig] 网络检测失败,使用默认配置(公网10MB):', error)
- return 10 * 1024 * 1024
- }
- }
-
- // 根据网络环境返回最优分片大小
- const chunkSize = network.isInternal
- ? 20 * 1024 * 1024 // 内网:20MB
- : 10 * 1024 * 1024 // 公网:10MB
-
- console.log(`[ChunkConfig] 检测到${network.isInternal ? '内网' : '公网'}访问,使用 ${chunkSize / 1024 / 1024}MB 分片`)
+ console.log(`[ChunkConfig] 使用 50MB 分片(统一配置)`)
return chunkSize
}
/**
- * 同步获取分片大小(如果网络状态已缓存)
- * 如果未检测过,返回默认值(公网10MB)
+ * 同步获取分片大小(统一50MB)
* @returns 分片大小(字节)
*/
export function getChunkSizeSync(): number {
- const network = getNetworkStatus()
-
- // 如果还没检测过,返回默认值(公网配置,更保守)
- if (!network.lastChecked) {
- console.warn('[ChunkConfig] 网络状态未检测,使用默认配置(公网10MB)')
- return 10 * 1024 * 1024
- }
-
- return network.isInternal
- ? 20 * 1024 * 1024 // 内网:20MB
- : 10 * 1024 * 1024 // 公网:10MB
+ const chunkSize = 50 * 1024 * 1024 // 50MB
+ console.log(`[ChunkConfig] 使用 50MB 分片(同步)`)
+ return chunkSize
}
/**
* 获取超时时间(根据分片大小动态调整)
- * 假设最低速度 500KB/s,加上50%缓冲
+ * 假设最低速度 300KB/s,加上100%缓冲(更保守)
* @param chunkSize 分片大小(字节)
* @returns 超时时间(毫秒)
*/
export function getChunkTimeout(chunkSize: number): number {
- const minSpeed = 500 * 1024 // 500KB/s(最低速度)
+ const minSpeed = 300 * 1024 // 300KB/s(更保守的最低速度)
const baseTimeout = (chunkSize / minSpeed) * 1000
- // 加上50%缓冲,确保不会轻易超时
- const timeout = Math.ceil(baseTimeout * 1.5)
+ // 加上100%缓冲,确保不会轻易超时
+ const timeout = Math.ceil(baseTimeout * 2)
- // 最小60秒,最大180秒
- return Math.max(60000, Math.min(180000, timeout))
+ // 最小5分钟,最大10分钟
+ return Math.max(300000, Math.min(600000, timeout))
}
/**
diff --git a/lib/chunk-upload.ts b/lib/chunk-upload.ts
index 7b52d1c..1973878 100644
--- a/lib/chunk-upload.ts
+++ b/lib/chunk-upload.ts
@@ -504,48 +504,64 @@ export async function chunkUpload(
throw error
}
- // 如果是会话过期,重新初始化上传会话
+ // 如果是会话过期,尝试刷新会话(保留进度)
if ((error as Error)?.message === 'UPLOAD_SESSION_EXPIRED') {
- // 如果已经有其他worker在重新初始化,等待它完成
+ // 如果已经有其他worker在处理,等待它完成
if (reinitPromise) {
- console.log(`[ChunkUpload] 等待其他worker完成会话重新初始化...`)
+ console.log(`[ChunkUpload] 等待其他worker完成会话刷新...`)
await reinitPromise
- // 重新初始化完成后,继续上传当前chunk
continue
}
-
- // 标记会话过期,防止其他worker重复初始化
+
+ // 标记会话过期,防止其他worker重复处理
if (!sessionExpired) {
sessionExpired = true
- console.warn(`[ChunkUpload] 🔄 检测到会话过期,重新初始化上传会话...`)
-
+ console.warn(`[ChunkUpload] 🔄 检测到会话过期,尝试刷新会话(保留已上传进度)...`)
+
reinitPromise = (async () => {
try {
- // 重新初始化
- state.uploadId = await initUpload(file.name, file.size, totalChunks)
- // 清空已上传记录(因为新会话不知道之前的进度)
- state.uploadedChunks.clear()
- completedBytes = 0
- await saveUploadState(state)
- // 将所有chunk重新加入队列
- pendingChunks.length = 0
- for (let i = 0; i < totalChunks; i++) {
- pendingChunks.push(i)
+ // 尝试刷新会话(保留uploadId和已上传进度)
+ const refreshResponse = await fetch('/api/materials/upload/refresh', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ uploadId: state.uploadId })
+ })
+
+ const refreshResult = await refreshResponse.json()
+
+ if (refreshResult.success) {
+ console.log(`[ChunkUpload] ✅ 会话刷新成功,保留 ${state.uploadedChunks.size} 个已上传分片,继续上传`)
+ sessionExpired = false
+ // 不清空 uploadedChunks,保留进度
+ return true
+ } else {
+ console.warn(`[ChunkUpload] ⚠️ 会话刷新失败(${refreshResult.error}),重新初始化上传...`)
+ // 刷新失败,需要重新初始化
+ state.uploadId = await initUpload(file.name, file.size, totalChunks)
+ state.uploadedChunks.clear()
+ completedBytes = 0
+ await saveUploadState(state)
+ // 将所有chunk重新加入队列
+ pendingChunks.length = 0
+ for (let i = 0; i < totalChunks; i++) {
+ pendingChunks.push(i)
+ }
+ console.log(`[ChunkUpload] ✅ 会话重新初始化成功,重新开始上传`)
+ sessionExpired = false
+ return false
}
- console.log(`[ChunkUpload] ✅ 会话重新初始化成功,重新开始上传`)
- sessionExpired = false
- } catch (reinitError) {
- console.error(`[ChunkUpload] ❌ 重新初始化失败:`, reinitError)
- throw new Error('上传会话已过期且无法重新初始化,请刷新页面后重试')
+ } catch (refreshError) {
+ console.error(`[ChunkUpload] ❌ 会话刷新/重新初始化失败:`, refreshError)
+ throw new Error('上传会话已过期且无法恢复,请刷新页面后重试')
}
})()
-
+
await reinitPromise
reinitPromise = null
// 不增加重试次数,直接继续
continue
} else {
- // 等待重新初始化完成
+ // 等待刷新/重新初始化完成
if (reinitPromise) {
await reinitPromise
continue
diff --git a/lib/upload-sessions-redis.ts b/lib/upload-sessions-redis.ts
index 26c6c92..9afaa59 100644
--- a/lib/upload-sessions-redis.ts
+++ b/lib/upload-sessions-redis.ts
@@ -37,7 +37,7 @@ const redis = new Redis(redisUrl, {
// Redis键前缀
const KEY_PREFIX = 'upload:session:'
-const EXPIRE_TIME = 5 * 60 // 5分钟(秒),基础过期时间 - 防止旧会话堆积
+const EXPIRE_TIME = 4 * 60 * 60 // 4小时(秒),支持大文件上传(17.5分钟×13≈220GB)
/**
* 保存上传会话
@@ -185,6 +185,27 @@ export async function refreshSessionTTL(uploadId: string): Promise {
}
}
+/**
+ * 刷新上传会话(保留已上传进度)
+ * 用于会话过期后恢复上传,不清空已上传的分片记录
+ */
+export async function refreshUploadSession(uploadId: string): Promise {
+ try {
+ const key = `${KEY_PREFIX}${uploadId}`
+ const exists = await redis.expire(key, EXPIRE_TIME)
+ if (exists === 1) {
+ console.log(`[Redis] 刷新上传会话成功: ${uploadId}, TTL: ${EXPIRE_TIME}秒`)
+ return true
+ } else {
+ console.warn(`[Redis] 刷新上传会话失败,会话不存在: ${uploadId}`)
+ return false
+ }
+ } catch (error) {
+ console.error(`[Redis] 刷新上传会话失败: ${uploadId}`, error)
+ return false
+ }
+}
+
/**
* 设置已完成会话的过期时间为 1 分钟(立即清理)
*/
diff --git a/lib/upload-sessions.ts b/lib/upload-sessions.ts
index 6c1afe0..6de509c 100644
--- a/lib/upload-sessions.ts
+++ b/lib/upload-sessions.ts
@@ -254,6 +254,32 @@ export async function updateChunk(
)
}
+/**
+ * 刷新上传会话(保留已上传进度)
+ * 用于会话过期后恢复上传,不清空已上传的分片记录
+ */
+export async function refreshUploadSession(uploadId: string): Promise {
+ return withRedisFallback(
+ 'refreshUploadSession',
+ async () => {
+ return await redisModule!.refreshUploadSession(uploadId)
+ },
+ async () => {
+ // 内存Map版本:更新过期时间
+ const session = memorySessions.get(uploadId)
+ if (session) {
+ // 内存会话自动延长4小时
+ const expireTime = 4 * 60 * 60 * 1000 // 4小时
+ session.createdAt = Date.now() - expireTime + (4 * 60 * 60 * 1000)
+ console.log(`[MemoryMap] 刷新上传会话成功: ${uploadId}`)
+ return true
+ }
+ console.warn(`[MemoryMap] 刷新上传会话失败,会话不存在: ${uploadId}`)
+ return false
+ }
+ )
+}
+
// 导出内存Map(用于向后兼容,但不推荐直接使用)
export const uploadSessions = memorySessions