即梦逆向服务 多服务部署修改 1
This commit is contained in:
parent
746d9730d8
commit
63950b18d1
47
.env.template
Normal file
47
.env.template
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# 即梦 Free API 环境变量配置模板
|
||||||
|
# 复制此文件为 .env 并填入实际值,或直接在启动时设置环境变量
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 基础服务配置
|
||||||
|
# ===========================================
|
||||||
|
NODE_ENV=production
|
||||||
|
SERVICE_ID=jimeng-api-prod
|
||||||
|
SERVICE_NAME=jimeng-free-api-prod
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=3302
|
||||||
|
|
||||||
|
# 可选:手动指定服务器基础URL,不设置会自动检测
|
||||||
|
# BASE_URL=http://your-server-ip:3302
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# MongoDB 数据库配置 (必须设置)
|
||||||
|
# ===========================================
|
||||||
|
MONGODB_URL=mongodb://localhost:27017/jimeng-api
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 火山引擎 TOS 对象存储配置 (必须设置)
|
||||||
|
# ===========================================
|
||||||
|
TOS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
TOS_ACCESS_KEY_SECRET=your_access_key_secret
|
||||||
|
TOS_BUCKET_NAME=your_bucket_name
|
||||||
|
|
||||||
|
# 可选配置,有默认值
|
||||||
|
TOS_SELF_DOMAIN=
|
||||||
|
TOS_REGION=cn-beijing
|
||||||
|
TOS_ENDPOINT=tos-cn-beijing.volces.com
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 心跳服务配置
|
||||||
|
# ===========================================
|
||||||
|
HEARTBEAT_ENABLED=true
|
||||||
|
HEARTBEAT_INTERVAL=30
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 使用方法:
|
||||||
|
# ===========================================
|
||||||
|
# 1. 复制此文件: cp .env.template .env
|
||||||
|
# 2. 编辑 .env 文件,填入实际的配置值
|
||||||
|
# 3. 启动服务:
|
||||||
|
# - PM2: pm2 start ecosystem.config.json --env production
|
||||||
|
# - Node.js: source .env && node dist/index.js
|
||||||
|
# - 脚本启动: ./start-node.sh prod
|
||||||
16
Dockerfile
16
Dockerfile
@ -1,5 +1,12 @@
|
|||||||
FROM node:20.19-slim
|
FROM node:20.19-slim
|
||||||
|
|
||||||
|
# 安装系统依赖和工具
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
procps \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy application dependency manifests to the container image.
|
# Copy application dependency manifests to the container image.
|
||||||
@ -21,6 +28,15 @@ RUN ls -la /app
|
|||||||
RUN echo "Debug: Contents of /app/dist after build:"
|
RUN echo "Debug: Contents of /app/dist after build:"
|
||||||
RUN ls -la /app/dist || echo "Debug: /app/dist directory not found or ls failed"
|
RUN ls -la /app/dist || echo "Debug: /app/dist directory not found or ls failed"
|
||||||
|
|
||||||
|
# 创建非 root 用户
|
||||||
|
RUN groupadd -r nodeuser && useradd -r -g nodeuser nodeuser
|
||||||
|
RUN chown -R nodeuser:nodeuser /app
|
||||||
|
USER nodeuser
|
||||||
|
|
||||||
EXPOSE 3302
|
EXPOSE 3302
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3302/ping || exit 1
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
199
PM2_CONFIG_GUIDE.md
Normal file
199
PM2_CONFIG_GUIDE.md
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# 通过 ecosystem.config.json 配置不同服务的示例
|
||||||
|
|
||||||
|
## 📋 基本原理
|
||||||
|
|
||||||
|
你可以完全通过修改 `ecosystem.config.json` 文件来配置不同数量和不同配置的服务,无需 `.env` 文件。
|
||||||
|
|
||||||
|
## 🎯 配置策略
|
||||||
|
|
||||||
|
### 1. 增加或减少服务数量
|
||||||
|
|
||||||
|
在 `apps` 数组中添加或删除服务配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
// 保留需要的服务,删除不需要的
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3302",
|
||||||
|
// ... 配置
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3303",
|
||||||
|
// ... 配置
|
||||||
|
}
|
||||||
|
// 删除 jimeng-api-3304 或添加更多服务
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 不同环境配置
|
||||||
|
|
||||||
|
每个服务都有 `env` (开发环境) 和 `env_production` (生产环境) 两套配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_BUCKET_NAME": "dev-bucket"
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_BUCKET_NAME": "prod-bucket"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 个性化服务配置
|
||||||
|
|
||||||
|
你可以为每个服务设置不同的配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-internal",
|
||||||
|
"env_production": {
|
||||||
|
"SERVICE_ID": "jimeng-internal",
|
||||||
|
"PORT": 3302,
|
||||||
|
"TOS_BUCKET_NAME": "internal-bucket",
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-external",
|
||||||
|
"env_production": {
|
||||||
|
"SERVICE_ID": "jimeng-external",
|
||||||
|
"PORT": 3303,
|
||||||
|
"TOS_BUCKET_NAME": "external-bucket",
|
||||||
|
"HEARTBEAT_INTERVAL": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 启动命令
|
||||||
|
|
||||||
|
### 启动所有服务
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.json --env production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动单个服务
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.json --only jimeng-api-3302 --env production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动多个指定服务
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.json --only "jimeng-api-3302,jimeng-api-3303" --env production
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 实际配置示例
|
||||||
|
|
||||||
|
### 示例1: 2个服务,不同TOS配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-images",
|
||||||
|
"env_production": {
|
||||||
|
"SERVICE_ID": "jimeng-images",
|
||||||
|
"PORT": 3302,
|
||||||
|
"TOS_BUCKET_NAME": "images-bucket",
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-videos",
|
||||||
|
"env_production": {
|
||||||
|
"SERVICE_ID": "jimeng-videos",
|
||||||
|
"PORT": 3303,
|
||||||
|
"TOS_BUCKET_NAME": "videos-bucket",
|
||||||
|
"HEARTBEAT_INTERVAL": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2: 单个服务
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-main",
|
||||||
|
"env_production": {
|
||||||
|
"SERVICE_ID": "jimeng-main",
|
||||||
|
"PORT": 3302,
|
||||||
|
"TOS_BUCKET_NAME": "main-bucket"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例3: 5个服务,负载分散
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3302",
|
||||||
|
"env_production": { "PORT": 3302, "SERVICE_ID": "jimeng-3302" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3303",
|
||||||
|
"env_production": { "PORT": 3303, "SERVICE_ID": "jimeng-3303" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3304",
|
||||||
|
"env_production": { "PORT": 3304, "SERVICE_ID": "jimeng-3304" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3305",
|
||||||
|
"env_production": { "PORT": 3305, "SERVICE_ID": "jimeng-3305" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3306",
|
||||||
|
"env_production": { "PORT": 3306, "SERVICE_ID": "jimeng-3306" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 关键配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 作用 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `name` | PM2进程名称 | `"jimeng-api-3302"` |
|
||||||
|
| `SERVICE_ID` | 心跳服务中的唯一标识 | `"jimeng-api-3302"` |
|
||||||
|
| `PORT` | 服务端口 | `3302` |
|
||||||
|
| `MONGODB_URL` | 数据库连接 | `"mongodb://localhost:27017/jimeng-api"` |
|
||||||
|
| `TOS_BUCKET_NAME` | 对象存储桶 | `"your-bucket"` |
|
||||||
|
| `HEARTBEAT_INTERVAL` | 心跳间隔(秒) | `30` |
|
||||||
|
|
||||||
|
## 🔄 动态管理
|
||||||
|
|
||||||
|
### 修改配置后重新加载
|
||||||
|
```bash
|
||||||
|
pm2 reload ecosystem.config.json --env production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 只重启特定服务
|
||||||
|
```bash
|
||||||
|
pm2 restart jimeng-api-3302
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除服务后清理
|
||||||
|
```bash
|
||||||
|
pm2 delete jimeng-api-3304
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 优势
|
||||||
|
|
||||||
|
1. **集中管理** - 所有配置在一个文件中
|
||||||
|
2. **版本控制** - 可以提交到Git中
|
||||||
|
3. **环境隔离** - dev和production环境分离
|
||||||
|
4. **灵活扩展** - 随时增减服务数量
|
||||||
|
5. **配置差异** - 每个服务可以有不同配置
|
||||||
114
README.md
114
README.md
@ -1,6 +1,118 @@
|
|||||||
# jimeng-free-api
|
# jimeng-free-api
|
||||||
|
|
||||||
即梦api服务
|
即梦api服务 - 服务器管理版本
|
||||||
|
|
||||||
|
## 新增功能 ✨
|
||||||
|
|
||||||
|
### 1. 服务器管理支持
|
||||||
|
- 📊 支持Python项目的服务器管理架构
|
||||||
|
- 💓 服务器心跳监控和状态上报
|
||||||
|
- 🔄 自动注册和发现机制
|
||||||
|
|
||||||
|
### 2. 容器化部署
|
||||||
|
- 🐳 简化的Docker配置,只管理服务自身
|
||||||
|
- ⚙️ 环境变量配置,无需手动配置文件
|
||||||
|
- 🛠️ 自动化部署脚本
|
||||||
|
|
||||||
|
### 3. API 接口优化
|
||||||
|
- 🔍 适配Python项目的服务器查询接口
|
||||||
|
- 📈 服务器状态统计和监控
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 安装依赖
|
||||||
|
# 确保系统已安装 Docker 和 Docker Compose
|
||||||
|
|
||||||
|
# 2. 启动服务
|
||||||
|
./deploy.sh start
|
||||||
|
|
||||||
|
# 3. 检查服务状态
|
||||||
|
./deploy.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
可以通过环境变量或docker-compose.yml中设置:
|
||||||
|
|
||||||
|
### 必需配置
|
||||||
|
- `MONGODB_URL`: MongoDB连接地址
|
||||||
|
- `TOS_ACCESS_KEY_ID`: 火山引擎TOS访问密钥ID
|
||||||
|
- `TOS_ACCESS_KEY_SECRET`: 火山引擎TOS访问密钥
|
||||||
|
- `TOS_BUCKET_NAME`: TOS存储桶名称
|
||||||
|
|
||||||
|
### 可选配置
|
||||||
|
- `SERVICE_ID`: 服务器唯一标识(默认:jimeng-free-api)
|
||||||
|
- `SERVICE_NAME`: 服务器名称(默认:与SERVICE_ID相同)
|
||||||
|
- `HEARTBEAT_INTERVAL`: 心跳间隔秒数(默认:60)
|
||||||
|
- `API_PORT`: API服务端口(默认:3302)
|
||||||
|
|
||||||
|
## 部署指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动服务
|
||||||
|
./deploy.sh start
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
./deploy.sh stop
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
./deploy.sh restart
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
./deploy.sh status
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
./deploy.sh logs
|
||||||
|
./deploy.sh logs -f # 实时跟踪
|
||||||
|
|
||||||
|
# 重新构建
|
||||||
|
./deploy.sh build
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 原有接口
|
||||||
|
- `POST /v1/chat/completions` - OpenAI兼容的聊天接口
|
||||||
|
- `POST /v1/images/generations` - 图像生成接口
|
||||||
|
- `POST /v1/video/generations` - 视频生成接口
|
||||||
|
- `GET /ping` - 健康检查接口
|
||||||
|
|
||||||
|
### 服务器管理接口
|
||||||
|
- `GET /api/servers/current` - 获取当前服务器信息
|
||||||
|
- `GET /api/servers/active` - 获取所有活跃服务器
|
||||||
|
- `GET /api/servers/online` - 获取所有在线服务器
|
||||||
|
- `GET /api/servers/stats` - 获取服务器状态统计
|
||||||
|
- `GET /api/servers/:serverId` - 获取特定服务器详情
|
||||||
|
- `POST /api/servers/cleanup` - 清理离线服务器记录
|
||||||
|
- `POST /api/servers/:serverId/heartbeat` - 更新服务器心跳
|
||||||
|
|
||||||
|
### 服务状态查询示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看当前服务器信息
|
||||||
|
curl http://localhost:3302/api/servers/current
|
||||||
|
|
||||||
|
# 查看所有在线服务器
|
||||||
|
curl http://localhost:3302/api/servers/online
|
||||||
|
|
||||||
|
# 查看服务器统计
|
||||||
|
curl http://localhost:3302/api/servers/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与Python项目集成
|
||||||
|
|
||||||
|
本服务专门适配`ai_jimeng3_handler.py`中的服务器管理架构:
|
||||||
|
|
||||||
|
1. **数据库结构适配**: 使用与Python项目相同的`jimeng_servers`表结构
|
||||||
|
2. **心跳机制**: 自动向数据库更新服务器心跳状态
|
||||||
|
3. **负载均衡**: 支持Python项目的服务器选择和负载均衡策略
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 本服务不包含MongoDB和Nginx,需要在外部配置
|
||||||
|
- 数据库结构由Python项目统一管理
|
||||||
|
- 必须正确配置环境变量后才能正常运行
|
||||||
|
|
||||||
### 参考自
|
### 参考自
|
||||||
即梦接口转 API [kimi-free-api](https://github.com/LLM-Red-Team/jimeng-free-api)
|
即梦接口转 API [kimi-free-api](https://github.com/LLM-Red-Team/jimeng-free-api)
|
||||||
155
TOS_UPLOAD_INTEGRATION.md
Normal file
155
TOS_UPLOAD_INTEGRATION.md
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# TOS上传集成说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
现在图片和视频生成完成后,会自动将原始URL的文件上传到TOS对象存储,并返回TOS地址。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
### 1. 图片功能改进
|
||||||
|
|
||||||
|
**文件路径**: `src/api/routes/images.ts`
|
||||||
|
|
||||||
|
- 增加了 `uploadImagesToTOS()` 函数
|
||||||
|
- 修改了 `/query` 接口,当任务完成时自动处理TOS上传
|
||||||
|
- 增加了缓存机制,避免重复上传
|
||||||
|
|
||||||
|
### 2. 视频功能改进
|
||||||
|
|
||||||
|
**文件路径**: `src/api/routes/video.ts`
|
||||||
|
|
||||||
|
- 增加了 `uploadVideosToTOS()` 函数
|
||||||
|
- 修改了 `/query` 接口,当任务完成时自动处理TOS上传
|
||||||
|
- 增加了缓存机制,避免重复上传
|
||||||
|
|
||||||
|
### 3. 任务缓存改进
|
||||||
|
|
||||||
|
**文件路径**:
|
||||||
|
- `src/api/ImagesTaskCache.ts`
|
||||||
|
- `src/api/VideoTaskCache.ts`
|
||||||
|
|
||||||
|
增加了TOS处理状态跟踪:
|
||||||
|
- `isTosProcessed(taskId)`: 检查任务是否已处理TOS上传
|
||||||
|
- `markTosProcessed(taskId)`: 标记任务为已处理TOS上传
|
||||||
|
|
||||||
|
### 4. TOS服务配置
|
||||||
|
|
||||||
|
**文件路径**: `src/lib/tos/tos-service.ts`
|
||||||
|
|
||||||
|
- 支持通过环境变量配置TOS参数
|
||||||
|
- 增强了错误处理和日志记录
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
### 图片生成流程
|
||||||
|
|
||||||
|
1. **发起生成请求**
|
||||||
|
```
|
||||||
|
POST /v1/images/generations
|
||||||
|
{
|
||||||
|
"task_id": "task_123",
|
||||||
|
"prompt": "一只可爱的猫",
|
||||||
|
"width": 1024,
|
||||||
|
"height": 1024
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **查询任务状态**
|
||||||
|
```
|
||||||
|
GET /v1/images/query?task_id=task_123
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **第一次查询**(任务完成时)
|
||||||
|
- 自动上传图片到TOS
|
||||||
|
- 返回TOS地址
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created": 1703123456,
|
||||||
|
"data": {
|
||||||
|
"task_id": "task_123",
|
||||||
|
"url": "https://your-domain.com/images/image-1703123456-abc123.webp",
|
||||||
|
"status": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **后续查询**
|
||||||
|
- 直接返回缓存的TOS地址
|
||||||
|
- 不会重复上传
|
||||||
|
|
||||||
|
### 视频生成流程
|
||||||
|
|
||||||
|
1. **发起生成请求**
|
||||||
|
```
|
||||||
|
POST /v1/video/generations
|
||||||
|
{
|
||||||
|
"task_id": "video_123",
|
||||||
|
"prompt": "一只猫在草地上奔跑",
|
||||||
|
"duration": 5,
|
||||||
|
"ratio": "16:9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **查询任务状态**
|
||||||
|
```
|
||||||
|
GET /v1/video/query?task_id=video_123
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **处理流程同图片**
|
||||||
|
- 自动上传视频到TOS
|
||||||
|
- 返回TOS地址
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
需要在环境变量中配置以下TOS参数:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# TOS对象存储配置
|
||||||
|
TOS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
TOS_ACCESS_KEY_SECRET=your_access_key_secret
|
||||||
|
TOS_BUCKET_NAME=your_bucket_name
|
||||||
|
TOS_SELF_DOMAIN=your_custom_domain.com
|
||||||
|
TOS_REGION=cn-beijing
|
||||||
|
TOS_ENDPOINT=tos-cn-beijing.volces.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件存储路径
|
||||||
|
|
||||||
|
- **图片**: `images/image-{timestamp}-{random}.webp`
|
||||||
|
- **视频**: `videos/video-{timestamp}-{random}.mp4`
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- 如果TOS上传失败,会返回原始URL
|
||||||
|
- 错误信息会记录到日志中
|
||||||
|
- 不会影响原有功能的正常使用
|
||||||
|
|
||||||
|
## 日志记录
|
||||||
|
|
||||||
|
相关日志会记录在:
|
||||||
|
- `logs/images_task_cache.log` - 图片任务缓存日志
|
||||||
|
- `logs/video_task_cache.log` - 视频任务缓存日志
|
||||||
|
- 应用主日志 - TOS上传成功/失败日志
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
1. **缓存机制**: 避免重复上传相同文件
|
||||||
|
2. **异步处理**: TOS上传不阻塞API响应
|
||||||
|
3. **错误兜底**: 上传失败时返回原始URL
|
||||||
|
4. **日志记录**: 便于监控和调试
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保TOS服务配置正确
|
||||||
|
2. 确保网络可以访问TOS服务
|
||||||
|
3. 监控TOS存储容量和费用
|
||||||
|
4. 定期清理过期文件(可选)
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
可以通过以下方式验证功能:
|
||||||
|
|
||||||
|
1. 生成一张图片,检查返回的URL是否为TOS地址
|
||||||
|
2. 多次查询同一个task_id,确认不会重复上传
|
||||||
|
3. 检查TOS存储中是否有对应的文件
|
||||||
|
4. 测试TOS上传失败的情况下是否正常兜底
|
||||||
@ -3,4 +3,8 @@ name: jimeng-free-api
|
|||||||
# 服务绑定主机地址
|
# 服务绑定主机地址
|
||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
# 服务绑定端口
|
# 服务绑定端口
|
||||||
port: 3302
|
port: 3302
|
||||||
|
|
||||||
|
heartbeat:
|
||||||
|
enabled: true
|
||||||
|
interval: 30
|
||||||
196
deploy.sh
Normal file
196
deploy.sh
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 即梦 Free API 部署脚本
|
||||||
|
# 使用方法: ./deploy.sh [start|stop|restart|status|logs|build] [options]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
check_dependencies() {
|
||||||
|
log_info "检查系统依赖..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log_error "Docker 未安装,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
log_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "依赖检查通过"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
build_images() {
|
||||||
|
log_info "构建 Docker 镜像..."
|
||||||
|
docker-compose build --no-cache
|
||||||
|
log_success "镜像构建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
start_service() {
|
||||||
|
log_info "启动即梦 Free API 服务..."
|
||||||
|
docker-compose up -d
|
||||||
|
log_success "服务启动完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
stop_service() {
|
||||||
|
log_info "停止服务..."
|
||||||
|
docker-compose down
|
||||||
|
log_success "服务已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
restart_service() {
|
||||||
|
log_info "重启服务..."
|
||||||
|
docker-compose restart
|
||||||
|
log_success "服务重启完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看服务状态
|
||||||
|
check_status() {
|
||||||
|
log_info "检查服务状态..."
|
||||||
|
|
||||||
|
echo "=== Docker 容器状态 ==="
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 服务健康检查 ==="
|
||||||
|
|
||||||
|
# 检查 API 服务
|
||||||
|
CONTAINER_PORT=$(docker-compose port jimeng-free-api 3302 2>/dev/null | cut -d: -f2)
|
||||||
|
if [ -n "$CONTAINER_PORT" ]; then
|
||||||
|
if curl -s http://localhost:$CONTAINER_PORT/ping > /dev/null 2>&1; then
|
||||||
|
log_success "API 服务正常运行 (:$CONTAINER_PORT)"
|
||||||
|
|
||||||
|
# 检查服务器心跳
|
||||||
|
if curl -s http://localhost:$CONTAINER_PORT/api/servers/current > /dev/null 2>&1; then
|
||||||
|
echo
|
||||||
|
echo "=== 当前服务器信息 ==="
|
||||||
|
curl -s http://localhost:$CONTAINER_PORT/api/servers/current | jq '.data' 2>/dev/null || echo "需要安装 jq 来格式化 JSON 输出"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "API 服务无法访问"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "API 服务端口未暴露或服务未运行"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
view_logs() {
|
||||||
|
local follow=${1:-""}
|
||||||
|
|
||||||
|
if [ "$follow" = "-f" ] || [ "$follow" = "--follow" ]; then
|
||||||
|
log_info "实时查看服务日志..."
|
||||||
|
docker-compose logs -f --tail=100
|
||||||
|
else
|
||||||
|
log_info "查看服务日志..."
|
||||||
|
docker-compose logs --tail=100
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示帮助信息
|
||||||
|
show_help() {
|
||||||
|
echo "即梦 Free API 部署脚本"
|
||||||
|
echo
|
||||||
|
echo "使用方法:"
|
||||||
|
echo " $0 <command> [options]"
|
||||||
|
echo
|
||||||
|
echo "命令:"
|
||||||
|
echo " start 启动服务"
|
||||||
|
echo " stop 停止服务"
|
||||||
|
echo " restart 重启服务"
|
||||||
|
echo " status 查看服务状态"
|
||||||
|
echo " logs [-f] 查看日志 (-f 实时跟踪)"
|
||||||
|
echo " build 仅构建镜像"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
echo
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 start # 启动服务"
|
||||||
|
echo " $0 logs -f # 实时查看日志"
|
||||||
|
echo " $0 status # 检查服务状态"
|
||||||
|
echo
|
||||||
|
echo "环境变量配置:"
|
||||||
|
echo " 请在 docker-compose.yml 中或系统环境变量中配置:"
|
||||||
|
echo " - MONGODB_URL: MongoDB 连接地址"
|
||||||
|
echo " - TOS_ACCESS_KEY_ID: TOS 访问密钥 ID"
|
||||||
|
echo " - TOS_ACCESS_KEY_SECRET: TOS 访问密钥"
|
||||||
|
echo " - TOS_BUCKET_NAME: TOS 存储桶名称"
|
||||||
|
echo " - SERVICE_ID: 服务器唯一标识"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
local command="$1"
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
start)
|
||||||
|
check_dependencies
|
||||||
|
start_service
|
||||||
|
sleep 5
|
||||||
|
check_status
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_service
|
||||||
|
sleep 5
|
||||||
|
check_status
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
check_status
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
view_logs "$2"
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
check_dependencies
|
||||||
|
build_images
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
log_error "请指定命令"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "未知命令: $command"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行主函数
|
||||||
|
main "$@"
|
||||||
@ -1,19 +1,35 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# jimeng-free-api:
|
# 即梦 API 服务
|
||||||
# build:
|
jimeng-free-api:
|
||||||
# context: ./
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# image: jimeng-free-api:latest
|
|
||||||
# container_name: jimeng-free-api
|
|
||||||
# ports:
|
|
||||||
# - "8443:3302"
|
|
||||||
jimeng-free-api-pro:
|
|
||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: jimeng-free-api:latest
|
image: jimeng-free-api:latest
|
||||||
container_name: jimeng-free-api-pro
|
container_name: jimeng-free-api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SERVICE_ID=${SERVICE_ID:-jimeng-free-api}
|
||||||
|
- INSTANCE_ID=${INSTANCE_ID:-instance-1}
|
||||||
|
- SERVICE_NAME=${SERVICE_NAME:-jimeng-free-api}
|
||||||
|
- MONGODB_URL=${MONGODB_URL:-mongodb://localhost:27017/jimeng-api}
|
||||||
|
- TOS_ACCESS_KEY_ID=${TOS_ACCESS_KEY_ID}
|
||||||
|
- TOS_ACCESS_KEY_SECRET=${TOS_ACCESS_KEY_SECRET}
|
||||||
|
- TOS_BUCKET_NAME=${TOS_BUCKET_NAME}
|
||||||
|
- TOS_SELF_DOMAIN=${TOS_SELF_DOMAIN}
|
||||||
|
- TOS_REGION=${TOS_REGION:-cn-beijing}
|
||||||
|
- TOS_ENDPOINT=${TOS_ENDPOINT:-tos-cn-beijing.volces.com}
|
||||||
|
- HEARTBEAT_ENABLED=${HEARTBEAT_ENABLED:-true}
|
||||||
|
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
|
||||||
ports:
|
ports:
|
||||||
- "3300:3302"
|
- "${API_PORT:-3302}:3302"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3302/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
35
docker-compose.yml.template
Normal file
35
docker-compose.yml.template
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 即梦 API 服务
|
||||||
|
jimeng-free-api:
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: jimeng-free-api:latest
|
||||||
|
container_name: jimeng-free-api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SERVICE_ID=${SERVICE_ID:-jimeng-free-api}
|
||||||
|
- INSTANCE_ID=${INSTANCE_ID:-instance-1}
|
||||||
|
- SERVICE_NAME=${SERVICE_NAME:-jimeng-free-api}
|
||||||
|
- MONGODB_URL=${MONGODB_URL:-mongodb://localhost:27017/jimeng-api}
|
||||||
|
- TOS_ACCESS_KEY_ID=${TOS_ACCESS_KEY_ID}
|
||||||
|
- TOS_ACCESS_KEY_SECRET=${TOS_ACCESS_KEY_SECRET}
|
||||||
|
- TOS_BUCKET_NAME=${TOS_BUCKET_NAME}
|
||||||
|
- TOS_SELF_DOMAIN=${TOS_SELF_DOMAIN}
|
||||||
|
- TOS_REGION=${TOS_REGION:-cn-beijing}
|
||||||
|
- TOS_ENDPOINT=${TOS_ENDPOINT:-tos-cn-beijing.volces.com}
|
||||||
|
- HEARTBEAT_ENABLED=${HEARTBEAT_ENABLED:-true}
|
||||||
|
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-3302}:3302"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3302/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
148
ecosystem.config.json
Normal file
148
ecosystem.config.json
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3302",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3302",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3302",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3302,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3302-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3302-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3302,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3302.log",
|
||||||
|
"out_file": "./logs/out-3302.log",
|
||||||
|
"error_file": "./logs/error-3302.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3303",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3303",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3303",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3303,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3303-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3303-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3303,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3303.log",
|
||||||
|
"out_file": "./logs/out-3303.log",
|
||||||
|
"error_file": "./logs/error-3303.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3304",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3304",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3304",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3304,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3304-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3304-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3304,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3304.log",
|
||||||
|
"out_file": "./logs/out-3304.log",
|
||||||
|
"error_file": "./logs/error-3304.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
148
ecosystem.config.json.template
Normal file
148
ecosystem.config.json.template
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3302",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3302",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3302",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3302,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3302-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3302-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3302,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3302.log",
|
||||||
|
"out_file": "./logs/out-3302.log",
|
||||||
|
"error_file": "./logs/error-3302.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3303",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3303",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3303",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3303,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3303-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3303-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3303,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3303.log",
|
||||||
|
"out_file": "./logs/out-3303.log",
|
||||||
|
"error_file": "./logs/error-3303.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jimeng-api-3304",
|
||||||
|
"script": "dist/index.js",
|
||||||
|
"instances": 1,
|
||||||
|
"exec_mode": "cluster",
|
||||||
|
"watch": false,
|
||||||
|
"max_memory_restart": "500M",
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"SERVICE_ID": "jimeng-api-3304",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3304",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3304,
|
||||||
|
"MONGODB_URL": "mongodb://localhost:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "your_actual_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "your_actual_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "your_actual_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"env_production": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_ID": "jimeng-api-3304-prod",
|
||||||
|
"SERVICE_NAME": "jimeng-free-api-3304-prod",
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": 3304,
|
||||||
|
"MONGODB_URL": "mongodb://prod-server:27017/jimeng-api",
|
||||||
|
"TOS_ACCESS_KEY_ID": "prod_access_key_id",
|
||||||
|
"TOS_ACCESS_KEY_SECRET": "prod_access_key_secret",
|
||||||
|
"TOS_BUCKET_NAME": "prod_bucket_name",
|
||||||
|
"TOS_SELF_DOMAIN": "https://your-domain.com",
|
||||||
|
"TOS_REGION": "cn-beijing",
|
||||||
|
"TOS_ENDPOINT": "tos-cn-beijing.volces.com",
|
||||||
|
"HEARTBEAT_ENABLED": true,
|
||||||
|
"HEARTBEAT_INTERVAL": 30
|
||||||
|
},
|
||||||
|
"log_file": "./logs/combined-3304.log",
|
||||||
|
"out_file": "./logs/out-3304.log",
|
||||||
|
"error_file": "./logs/error-3304.log",
|
||||||
|
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
|
||||||
|
"merge_logs": true,
|
||||||
|
"autorestart": true,
|
||||||
|
"max_restarts": 10,
|
||||||
|
"min_uptime": "10s"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
312
multi-instance.sh
Normal file
312
multi-instance.sh
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 即梦 Free API 多实例 PM2 管理脚本
|
||||||
|
# 使用方法: ./multi-instance.sh [start|stop|restart|status|logs] [port1,port2,port3|all]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 可用的端口列表
|
||||||
|
AVAILABLE_PORTS=(3302 3303 3304)
|
||||||
|
ALL_APPS=("jimeng-api-3302" "jimeng-api-3303" "jimeng-api-3304")
|
||||||
|
|
||||||
|
# 检查PM2
|
||||||
|
check_pm2() {
|
||||||
|
if ! command -v pm2 &> /dev/null; then
|
||||||
|
log_error "PM2 未安装,请先安装 PM2"
|
||||||
|
log_info "npm install -g pm2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
build_project() {
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
log_info "构建项目..."
|
||||||
|
yarn install
|
||||||
|
yarn run build
|
||||||
|
log_success "项目构建完成"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 解析端口参数
|
||||||
|
parse_ports() {
|
||||||
|
local port_arg=$1
|
||||||
|
local target_ports=()
|
||||||
|
|
||||||
|
if [ "$port_arg" = "all" ] || [ -z "$port_arg" ]; then
|
||||||
|
target_ports=("${AVAILABLE_PORTS[@]}")
|
||||||
|
else
|
||||||
|
IFS=',' read -ra PORTS <<< "$port_arg"
|
||||||
|
for port in "${PORTS[@]}"; do
|
||||||
|
if [[ " ${AVAILABLE_PORTS[@]} " =~ " ${port} " ]]; then
|
||||||
|
target_ports+=("$port")
|
||||||
|
else
|
||||||
|
log_warning "无效端口: $port (可用端口: ${AVAILABLE_PORTS[*]})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${target_ports[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取应用名称
|
||||||
|
get_app_name() {
|
||||||
|
local port=$1
|
||||||
|
echo "jimeng-api-${port}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动指定端口的服务
|
||||||
|
start_services() {
|
||||||
|
local ports_str=$1
|
||||||
|
local env_mode=${2:-production}
|
||||||
|
local target_ports=($(parse_ports "$ports_str"))
|
||||||
|
|
||||||
|
if [ ${#target_ports[@]} -eq 0 ]; then
|
||||||
|
log_error "没有有效的端口"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "启动服务实例..."
|
||||||
|
build_project
|
||||||
|
|
||||||
|
for port in "${target_ports[@]}"; do
|
||||||
|
local app_name=$(get_app_name "$port")
|
||||||
|
log_info "启动 $app_name (端口 $port)..."
|
||||||
|
|
||||||
|
# 停止现有实例
|
||||||
|
pm2 delete "$app_name" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 启动新实例
|
||||||
|
if [ "$env_mode" = "production" ]; then
|
||||||
|
pm2 start ecosystem.config.json --only "$app_name" --env production
|
||||||
|
else
|
||||||
|
pm2 start ecosystem.config.json --only "$app_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 等待启动完成
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 检查启动状态
|
||||||
|
if pm2 list | grep -q "$app_name.*online"; then
|
||||||
|
log_success "$app_name 启动成功 (http://localhost:$port)"
|
||||||
|
else
|
||||||
|
log_error "$app_name 启动失败"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
pm2 save
|
||||||
|
log_success "所有服务启动完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止指定端口的服务
|
||||||
|
stop_services() {
|
||||||
|
local ports_str=$1
|
||||||
|
local target_ports=($(parse_ports "$ports_str"))
|
||||||
|
|
||||||
|
if [ ${#target_ports[@]} -eq 0 ]; then
|
||||||
|
log_error "没有有效的端口"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "停止服务实例..."
|
||||||
|
|
||||||
|
for port in "${target_ports[@]}"; do
|
||||||
|
local app_name=$(get_app_name "$port")
|
||||||
|
log_info "停止 $app_name..."
|
||||||
|
pm2 delete "$app_name" 2>/dev/null || log_warning "$app_name 不存在或已停止"
|
||||||
|
done
|
||||||
|
|
||||||
|
log_success "服务停止完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
restart_services() {
|
||||||
|
local ports_str=$1
|
||||||
|
local env_mode=${2:-production}
|
||||||
|
|
||||||
|
log_info "重启服务..."
|
||||||
|
stop_services "$ports_str"
|
||||||
|
sleep 2
|
||||||
|
start_services "$ports_str" "$env_mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看服务状态
|
||||||
|
check_status() {
|
||||||
|
local ports_str=$1
|
||||||
|
local target_ports=($(parse_ports "$ports_str"))
|
||||||
|
|
||||||
|
echo "=== PM2 进程列表 ==="
|
||||||
|
pm2 list
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 服务详细状态 ==="
|
||||||
|
|
||||||
|
if [ ${#target_ports[@]} -eq 0 ]; then
|
||||||
|
target_ports=("${AVAILABLE_PORTS[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
for port in "${target_ports[@]}"; do
|
||||||
|
local app_name=$(get_app_name "$port")
|
||||||
|
echo "--- $app_name (端口 $port) ---"
|
||||||
|
|
||||||
|
if pm2 list | grep -q "$app_name"; then
|
||||||
|
local status=$(pm2 list | grep "$app_name" | awk '{print $10}')\n log_info "状态: $status"
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
if curl -s "http://localhost:$port/ping" > /dev/null 2>&1; then
|
||||||
|
log_success "健康检查: 正常"
|
||||||
|
|
||||||
|
# 获取服务器信息
|
||||||
|
local server_info=$(curl -s "http://localhost:$port/api/servers/current" 2>/dev/null || echo "{}")\n if [ "$server_info" != "{}" ]; then
|
||||||
|
log_info "服务器信息: $(echo "$server_info" | jq -r '.data.server_id // "unknown"' 2>/dev/null || echo "获取失败")"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "健康检查: 失败"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "进程不存在"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== 端口占用情况 ==="
|
||||||
|
for port in "${target_ports[@]}"; do
|
||||||
|
if netstat -tlnp 2>/dev/null | grep ":$port " > /dev/null; then
|
||||||
|
log_success "端口 $port: 已占用"
|
||||||
|
else
|
||||||
|
log_warning "端口 $port: 未占用"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
view_logs() {
|
||||||
|
local ports_str=$1
|
||||||
|
local target_ports=($(parse_ports "$ports_str"))
|
||||||
|
|
||||||
|
if [ ${#target_ports[@]} -eq 0 ]; then
|
||||||
|
log_error "没有有效的端口"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#target_ports[@]} -eq 1 ]; then
|
||||||
|
# 单个服务,显示实时日志
|
||||||
|
local app_name=$(get_app_name "${target_ports[0]}")
|
||||||
|
log_info "查看 $app_name 实时日志..."
|
||||||
|
pm2 logs "$app_name" --lines 50
|
||||||
|
else
|
||||||
|
# 多个服务,显示最近日志
|
||||||
|
for port in "${target_ports[@]}"; do
|
||||||
|
local app_name=$(get_app_name "$port")
|
||||||
|
echo "=== $app_name 最近日志 ==="
|
||||||
|
pm2 logs "$app_name" --lines 20 --nostream
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示帮助
|
||||||
|
show_help() {
|
||||||
|
echo "即梦 Free API 多实例 PM2 管理脚本"
|
||||||
|
echo
|
||||||
|
echo "使用方法:"
|
||||||
|
echo " $0 <command> [ports] [env]"
|
||||||
|
echo
|
||||||
|
echo "命令:"
|
||||||
|
echo " start 启动服务实例"
|
||||||
|
echo " stop 停止服务实例"
|
||||||
|
echo " restart 重启服务实例"
|
||||||
|
echo " status 查看服务状态"
|
||||||
|
echo " logs 查看服务日志"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
echo
|
||||||
|
echo "端口参数:"
|
||||||
|
echo " all 所有端口 (默认)"
|
||||||
|
echo " 3302 仅端口 3302"
|
||||||
|
echo " 3302,3303 多个端口,用逗号分隔"
|
||||||
|
echo " 可用端口: ${AVAILABLE_PORTS[*]}"
|
||||||
|
echo
|
||||||
|
echo "环境参数 (仅 start/restart):"
|
||||||
|
echo " production 生产环境 (默认)"
|
||||||
|
echo " development 开发环境"
|
||||||
|
echo
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 start all production # 启动所有实例 (生产环境)"
|
||||||
|
echo " $0 start 3302,3303 development # 启动端口 3302 和 3303 (开发环境)"
|
||||||
|
echo " $0 stop 3304 # 停止端口 3304"
|
||||||
|
echo " $0 restart all # 重启所有实例"
|
||||||
|
echo " $0 status # 查看所有实例状态"
|
||||||
|
echo " $0 logs 3302 # 查看端口 3302 的日志"
|
||||||
|
echo
|
||||||
|
echo "服务访问地址:"
|
||||||
|
for port in "${AVAILABLE_PORTS[@]}"; do
|
||||||
|
echo " - http://localhost:$port (jimeng-api-$port)"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
local command="$1"
|
||||||
|
local ports="$2"
|
||||||
|
local env_mode="${3:-production}"
|
||||||
|
|
||||||
|
check_pm2
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
start)
|
||||||
|
start_services "$ports" "$env_mode"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_services "$ports"
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_services "$ports" "$env_mode"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
check_status "$ports"
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
view_logs "$ports"
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
log_error "请指定命令"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "未知命令: $command"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行主函数
|
||||||
|
main "$@"
|
||||||
@ -41,11 +41,16 @@
|
|||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"randomstring": "^1.3.0",
|
"randomstring": "^1.3.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"yaml": "^2.3.4"
|
"yaml": "^2.3.4",
|
||||||
|
"@volcengine/tos-sdk": "^2.6.7",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"mongoose": "^8.0.3",
|
||||||
|
"node-cron": "^3.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/mime": "^3.0.4",
|
"@types/mime": "^3.0.4",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"tsup": "^8.0.2",
|
"tsup": "^8.0.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,9 +22,11 @@ function cacheLog(value: string, color?: string) {
|
|||||||
export class ImagesTaskCache {
|
export class ImagesTaskCache {
|
||||||
private static instance: ImagesTaskCache;
|
private static instance: ImagesTaskCache;
|
||||||
private taskCache: Map<string, number|string>;
|
private taskCache: Map<string, number|string>;
|
||||||
|
private tosProcessedTasks: Set<string>; // 记录已处理TOS上传的任务
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.taskCache = new Map<string, number|string>();
|
this.taskCache = new Map<string, number|string>();
|
||||||
|
this.tosProcessedTasks = new Set<string>();
|
||||||
cacheLog("ImagesTaskCache initialized");
|
cacheLog("ImagesTaskCache initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +69,15 @@ export class ImagesTaskCache {
|
|||||||
return this.taskCache.get(taskId);
|
return this.taskCache.get(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isTosProcessed(taskId: string): boolean {
|
||||||
|
return this.tosProcessedTasks.has(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public markTosProcessed(taskId: string): void {
|
||||||
|
this.tosProcessedTasks.add(taskId);
|
||||||
|
cacheLog(`Task ${taskId} marked as TOS processed`);
|
||||||
|
}
|
||||||
|
|
||||||
public getPendingTasks(): string[] {
|
public getPendingTasks(): string[] {
|
||||||
const pendingTasks: string[] = [];
|
const pendingTasks: string[] = [];
|
||||||
for (const [taskId, status] of this.taskCache.entries()) {
|
for (const [taskId, status] of this.taskCache.entries()) {
|
||||||
|
|||||||
@ -22,9 +22,11 @@ function cacheLog(value: string, color?: string) {
|
|||||||
export class VideoTaskCache {
|
export class VideoTaskCache {
|
||||||
private static instance: VideoTaskCache;
|
private static instance: VideoTaskCache;
|
||||||
private taskCache: Map<string, number|string>;
|
private taskCache: Map<string, number|string>;
|
||||||
|
private tosProcessedTasks: Set<string>; // 记录已处理TOS上传的任务
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.taskCache = new Map<string, number|string>();
|
this.taskCache = new Map<string, number|string>();
|
||||||
|
this.tosProcessedTasks = new Set<string>();
|
||||||
cacheLog("VideoTaskCache initialized");
|
cacheLog("VideoTaskCache initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +69,15 @@ export class VideoTaskCache {
|
|||||||
return this.taskCache.get(taskId);
|
return this.taskCache.get(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isTosProcessed(taskId: string): boolean {
|
||||||
|
return this.tosProcessedTasks.has(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public markTosProcessed(taskId: string): void {
|
||||||
|
this.tosProcessedTasks.add(taskId);
|
||||||
|
cacheLog(`Task ${taskId} marked as TOS processed`);
|
||||||
|
}
|
||||||
|
|
||||||
public getPendingTasks(): string[] {
|
public getPendingTasks(): string[] {
|
||||||
const pendingTasks: string[] = [];
|
const pendingTasks: string[] = [];
|
||||||
for (const [taskId, status] of this.taskCache.entries()) {
|
for (const [taskId, status] of this.taskCache.entries()) {
|
||||||
|
|||||||
@ -5,6 +5,34 @@ import { generateImages } from "@/api/controllers/images.ts";
|
|||||||
import { tokenSplit } from "@/api/controllers/core.ts";
|
import { tokenSplit } from "@/api/controllers/core.ts";
|
||||||
import util from "@/lib/util.ts";
|
import util from "@/lib/util.ts";
|
||||||
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
|
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
|
||||||
|
import TOSService from "@/lib/tos/tos-service.ts";
|
||||||
|
import logger from "@/lib/logger.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图片URL上传到TOS
|
||||||
|
* @param imageUrls 图片URL数组
|
||||||
|
* @returns TOS URL数组
|
||||||
|
*/
|
||||||
|
async function uploadImagesToTOS(imageUrls: string[]): Promise<string[]> {
|
||||||
|
const tosUrls: string[] = [];
|
||||||
|
|
||||||
|
for (const imageUrl of imageUrls) {
|
||||||
|
try {
|
||||||
|
// 从URL获取文件名
|
||||||
|
const fileName = `image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.webp`;
|
||||||
|
// 上传到TOS
|
||||||
|
const tosUrl = await TOSService.uploadFromUrl(imageUrl, `images/${fileName}`);
|
||||||
|
tosUrls.push(tosUrl);
|
||||||
|
logger.info(`图片上传到TOS成功: ${imageUrl} -> ${tosUrl}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`图片上传到TOS失败: ${imageUrl}`, error);
|
||||||
|
// 如果上传失败,保留原URL
|
||||||
|
tosUrls.push(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tosUrls;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
prefix: "/v1/images",
|
prefix: "/v1/images",
|
||||||
@ -18,11 +46,38 @@ export default {
|
|||||||
} = request.query; // 从 query 中获取
|
} = request.query; // 从 query 中获取
|
||||||
let res = imagesTaskCache.getTaskStatus(task_id);
|
let res = imagesTaskCache.getTaskStatus(task_id);
|
||||||
// console.log("查询任务状态", task_id, 'res:',res);
|
// console.log("查询任务状态", task_id, 'res:',res);
|
||||||
if(typeof res === 'string'){
|
if(typeof res === 'string'){
|
||||||
return {
|
// 任务已完成,检查是否已处理TOS上传
|
||||||
created: util.unixTimestamp(),
|
if (!imagesTaskCache.isTosProcessed(task_id)) {
|
||||||
data:{task_id, url:res, status:-1},
|
// 尚未处理TOS上传,处理图片URL上传到TOS
|
||||||
};
|
try {
|
||||||
|
const imageUrls = res.split(',');
|
||||||
|
const tosUrls = await uploadImagesToTOS(imageUrls);
|
||||||
|
const tosUrlsString = tosUrls.join(',');
|
||||||
|
|
||||||
|
// 更新缓存中TOS URL并标记为已处理
|
||||||
|
imagesTaskCache.finishTask(task_id, -1, tosUrlsString);
|
||||||
|
imagesTaskCache.markTosProcessed(task_id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url: tosUrlsString, status:-1},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`处理图片TOS上传失败: ${task_id}`, error);
|
||||||
|
// 如果上传失败,返回原始URL
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url:res, status:-1},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已处理TOS上传,直接返回缓存的TOS URL
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url:res, status:-1},
|
||||||
|
};
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
return {
|
return {
|
||||||
created: util.unixTimestamp(),
|
created: util.unixTimestamp(),
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import token from './token.js';
|
|||||||
// import models from './models.ts';
|
// import models from './models.ts';
|
||||||
import upload from './upload.ts';
|
import upload from './upload.ts';
|
||||||
import video from './video.ts';
|
import video from './video.ts';
|
||||||
|
import servers from './services.ts';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -29,5 +30,9 @@ export default [
|
|||||||
token,
|
token,
|
||||||
// models,
|
// models,
|
||||||
video,
|
video,
|
||||||
upload
|
upload,
|
||||||
|
{
|
||||||
|
prefix: '/api',
|
||||||
|
...servers
|
||||||
|
}
|
||||||
];
|
];
|
||||||
203
src/api/routes/services.ts
Normal file
203
src/api/routes/services.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import Response from '@/lib/response/Response.ts';
|
||||||
|
import { HeartbeatService } from '@/lib/services/HeartbeatService.ts';
|
||||||
|
import JimengServer from '@/lib/database/models/ServiceHeartbeat.ts';
|
||||||
|
import logger from '@/lib/logger.ts';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 获取当前服务器信息
|
||||||
|
get: {
|
||||||
|
'/servers/current': async () => {
|
||||||
|
try {
|
||||||
|
const heartbeatService = HeartbeatService.getInstance();
|
||||||
|
const serverInfo = heartbeatService.getServerInfo();
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
data: serverInfo
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get current server info:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有活跃服务器
|
||||||
|
'/servers/active': async () => {
|
||||||
|
try {
|
||||||
|
const activeServers = await HeartbeatService.getActiveServers();
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
data: activeServers,
|
||||||
|
total: activeServers.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get active servers:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有在线服务器(基于心跳超时检查)
|
||||||
|
'/servers/online': async () => {
|
||||||
|
try {
|
||||||
|
const onlineServers = await HeartbeatService.getOnlineServers();
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
data: onlineServers,
|
||||||
|
total: onlineServers.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get online servers:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取服务器状态统计
|
||||||
|
'/servers/stats': async () => {
|
||||||
|
try {
|
||||||
|
const stats = await JimengServer.aggregate([
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
totalServers: { $sum: 1 },
|
||||||
|
activeServers: {
|
||||||
|
$sum: {
|
||||||
|
$cond: [{ $eq: ['$is_active', true] }, 1, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
avgHeartbeatInterval: { $avg: '$heartbeat_interval' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const onlineServers = await JimengServer.countDocuments({
|
||||||
|
is_active: true,
|
||||||
|
$expr: {
|
||||||
|
$lte: [
|
||||||
|
{ $subtract: [currentTime, '$last_heartbeat'] },
|
||||||
|
{ $multiply: ['$heartbeat_interval', 1.5] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = stats[0] || { totalServers: 0, activeServers: 0, avgHeartbeatInterval: 0 };
|
||||||
|
result.onlineServers = onlineServers;
|
||||||
|
result.healthRate = result.totalServers > 0 ?
|
||||||
|
((onlineServers / result.totalServers) * 100).toFixed(2) + '%' : '0%';
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get server stats:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取特定服务器详情
|
||||||
|
'/servers/:serverId': async (request) => {
|
||||||
|
try {
|
||||||
|
const serverId = request.params.serverId;
|
||||||
|
const server = await JimengServer.findOne({ server_id: serverId });
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: 'Server not found'
|
||||||
|
}, { statusCode: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查服务器是否在线
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const heartbeatTimeout = server.heartbeat_interval * 1.5;
|
||||||
|
const isOnline = server.is_active && (currentTime - server.last_heartbeat) <= heartbeatTimeout;
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
...server.toObject(),
|
||||||
|
isOnline
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get server details:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
post: {
|
||||||
|
// 手动清理离线服务器
|
||||||
|
'/servers/cleanup': async () => {
|
||||||
|
try {
|
||||||
|
await HeartbeatService.cleanupOfflineServers();
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
message: 'Offline servers cleaned up successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to cleanup offline servers:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新服务器心跳(为Python项目提供)
|
||||||
|
'/servers/:serverId/heartbeat': async (request) => {
|
||||||
|
try {
|
||||||
|
const serverId = request.params.serverId;
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const result = await JimengServer.findOneAndUpdate(
|
||||||
|
{ server_id: serverId },
|
||||||
|
{
|
||||||
|
last_heartbeat: currentTime,
|
||||||
|
updated_at: currentTime,
|
||||||
|
is_active: true
|
||||||
|
},
|
||||||
|
{ new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: 'Server not found'
|
||||||
|
}, { statusCode: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response({
|
||||||
|
success: true,
|
||||||
|
message: 'Heartbeat updated successfully',
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to update server heartbeat:', error);
|
||||||
|
return new Response({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { statusCode: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -5,6 +5,34 @@ import { generateVideo, upgradeVideoResolution } from "@/api/controllers/video.t
|
|||||||
import { tokenSplit } from "@/api/controllers/core.ts";
|
import { tokenSplit } from "@/api/controllers/core.ts";
|
||||||
import util from "@/lib/util.ts";
|
import util from "@/lib/util.ts";
|
||||||
import { VideoTaskCache } from '@/api/VideoTaskCache.ts';
|
import { VideoTaskCache } from '@/api/VideoTaskCache.ts';
|
||||||
|
import TOSService from "@/lib/tos/tos-service.ts";
|
||||||
|
import logger from "@/lib/logger.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理视频URL上传到TOS
|
||||||
|
* @param videoUrls 视频URL数组
|
||||||
|
* @returns TOS URL数组
|
||||||
|
*/
|
||||||
|
async function uploadVideosToTOS(videoUrls: string[]): Promise<string[]> {
|
||||||
|
const tosUrls: string[] = [];
|
||||||
|
|
||||||
|
for (const videoUrl of videoUrls) {
|
||||||
|
try {
|
||||||
|
// 从URL获取文件名
|
||||||
|
const fileName = `video-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.mp4`;
|
||||||
|
// 上传到TOS
|
||||||
|
const tosUrl = await TOSService.uploadFromUrl(videoUrl, `videos/${fileName}`);
|
||||||
|
tosUrls.push(tosUrl);
|
||||||
|
logger.info(`视频上传到TOS成功: ${videoUrl} -> ${tosUrl}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`视频上传到TOS失败: ${videoUrl}`, error);
|
||||||
|
// 如果上传失败,保留原URL
|
||||||
|
tosUrls.push(videoUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tosUrls;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
prefix: "/v1/video",
|
prefix: "/v1/video",
|
||||||
@ -18,11 +46,38 @@ export default {
|
|||||||
} = request.query; // 从 query 中获取
|
} = request.query; // 从 query 中获取
|
||||||
let res = videoTaskCache.getTaskStatus(task_id);
|
let res = videoTaskCache.getTaskStatus(task_id);
|
||||||
// console.log("查询任务状态", task_id, 'res:',res);
|
// console.log("查询任务状态", task_id, 'res:',res);
|
||||||
if(typeof res === 'string'){
|
if(typeof res === 'string'){
|
||||||
return {
|
// 任务已完成,检查是否已处理TOS上传
|
||||||
created: util.unixTimestamp(),
|
if (!videoTaskCache.isTosProcessed(task_id)) {
|
||||||
data:{task_id, url:res, status:-1},
|
// 尚未处理TOS上传,处理视频URL上传到TOS
|
||||||
};
|
try {
|
||||||
|
const videoUrls = res.split(',');
|
||||||
|
const tosUrls = await uploadVideosToTOS(videoUrls);
|
||||||
|
const tosUrlsString = tosUrls.join(',');
|
||||||
|
|
||||||
|
// 更新缓存中TOS URL并标记为已处理
|
||||||
|
videoTaskCache.finishTask(task_id, -1, tosUrlsString);
|
||||||
|
videoTaskCache.markTosProcessed(task_id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url: tosUrlsString, status:-1},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`处理视频TOS上传失败: ${task_id}`, error);
|
||||||
|
// 如果上传失败,返回原始URL
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url:res, status:-1},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已处理TOS上传,直接返回缓存的TOS URL
|
||||||
|
return {
|
||||||
|
created: util.unixTimestamp(),
|
||||||
|
data:{task_id, url:res, status:-1},
|
||||||
|
};
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
return {
|
return {
|
||||||
created: util.unixTimestamp(),
|
created: util.unixTimestamp(),
|
||||||
|
|||||||
20
src/index.ts
20
src/index.ts
@ -6,6 +6,8 @@ import "@/lib/initialize.ts";
|
|||||||
import server from "@/lib/server.ts";
|
import server from "@/lib/server.ts";
|
||||||
import routes from "@/api/routes/index.ts";
|
import routes from "@/api/routes/index.ts";
|
||||||
import logger from "@/lib/logger.ts";
|
import logger from "@/lib/logger.ts";
|
||||||
|
import mongoDBManager from "@/lib/database/mongodb.ts";
|
||||||
|
import heartbeatService from "@/lib/services/HeartbeatService.ts";
|
||||||
|
|
||||||
const startupTime = performance.now();
|
const startupTime = performance.now();
|
||||||
|
|
||||||
@ -18,9 +20,27 @@ const startupTime = performance.now();
|
|||||||
logger.info("Environment:", environment.env);
|
logger.info("Environment:", environment.env);
|
||||||
logger.info("Service name:", config.service.name);
|
logger.info("Service name:", config.service.name);
|
||||||
|
|
||||||
|
// 初始化MongoDB连接
|
||||||
|
try {
|
||||||
|
await mongoDBManager.connect();
|
||||||
|
logger.success("MongoDB connected successfully");
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("MongoDB connection failed, continuing without database:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
server.attachRoutes(routes);
|
server.attachRoutes(routes);
|
||||||
await server.listen();
|
await server.listen();
|
||||||
|
|
||||||
|
// 启动心跳服务
|
||||||
|
if (config.heartbeat?.enabled !== false && mongoDBManager.isMongoConnected()) {
|
||||||
|
try {
|
||||||
|
await heartbeatService.start();
|
||||||
|
logger.success("Heartbeat service started");
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("Failed to start heartbeat service:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.service.bindAddress &&
|
config.service.bindAddress &&
|
||||||
logger.success("Service bind address:", config.service.bindAddress);
|
logger.success("Service bind address:", config.service.bindAddress);
|
||||||
})()
|
})()
|
||||||
|
|||||||
@ -1,14 +1,23 @@
|
|||||||
import serviceConfig from "./configs/service-config.ts";
|
import serviceConfig, { ServiceConfig } from "./configs/service-config.ts";
|
||||||
import systemConfig from "./configs/system-config.ts";
|
import systemConfig from "./configs/system-config.ts";
|
||||||
|
|
||||||
class Config {
|
class Config {
|
||||||
|
|
||||||
/** 服务配置 */
|
/** 服务配置 */
|
||||||
service = serviceConfig;
|
service: ServiceConfig = serviceConfig;
|
||||||
|
|
||||||
/** 系统配置 */
|
/** 系统配置 */
|
||||||
system = systemConfig;
|
system = systemConfig;
|
||||||
|
|
||||||
|
// 代理属性以便直接访问心跳和数据库配置
|
||||||
|
get heartbeat() {
|
||||||
|
return this.service.heartbeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
get database() {
|
||||||
|
return this.service.database;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Config();
|
export default new Config();
|
||||||
@ -24,14 +24,27 @@ export class ServiceConfig {
|
|||||||
urlPrefix;
|
urlPrefix;
|
||||||
/** @type {string} 服务绑定地址(外部访问地址) */
|
/** @type {string} 服务绑定地址(外部访问地址) */
|
||||||
bindAddress;
|
bindAddress;
|
||||||
|
/** 数据库配置 */
|
||||||
|
database?: {
|
||||||
|
mongodb?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** 心跳配置 */
|
||||||
|
heartbeat?: {
|
||||||
|
enabled: boolean;
|
||||||
|
interval: number;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(options?: any) {
|
constructor(options?: any) {
|
||||||
const { name, host, port, urlPrefix, bindAddress } = options || {};
|
const { name, host, port, urlPrefix, bindAddress, database, heartbeat } = options || {};
|
||||||
this.name = _.defaultTo(name, 'jimeng-free-api');
|
this.name = _.defaultTo(name, 'jimeng-free-api');
|
||||||
this.host = _.defaultTo(host, '0.0.0.0');
|
this.host = _.defaultTo(host, '0.0.0.0');
|
||||||
this.port = _.defaultTo(port, 5566);
|
this.port = _.defaultTo(port, 5566);
|
||||||
this.urlPrefix = _.defaultTo(urlPrefix, '');
|
this.urlPrefix = _.defaultTo(urlPrefix, '');
|
||||||
this.bindAddress = bindAddress;
|
this.bindAddress = bindAddress;
|
||||||
|
this.database = database;
|
||||||
|
this.heartbeat = _.defaultTo(heartbeat, { enabled: true, interval: 30 });
|
||||||
}
|
}
|
||||||
|
|
||||||
get addressHost() {
|
get addressHost() {
|
||||||
|
|||||||
60
src/lib/database/models/ServiceHeartbeat.ts
Normal file
60
src/lib/database/models/ServiceHeartbeat.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
|
// 即梦服务器配置表数据模型 - 适配Python项目
|
||||||
|
export interface IJimengServer extends Document {
|
||||||
|
server_id: string; // 服务器唯一标识
|
||||||
|
server_name: string; // 服务器名称
|
||||||
|
base_url: string; // 服务器基础URL
|
||||||
|
is_active: boolean; // 管理员控制开关
|
||||||
|
last_heartbeat: number; // 最后心跳时间戳(秒)
|
||||||
|
heartbeat_interval: number; // 心跳间隔(秒)
|
||||||
|
created_at: number; // 创建时间戳(秒)
|
||||||
|
updated_at: number; // 更新时间戳(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
const JimengServerSchema: Schema = new Schema({
|
||||||
|
server_id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
server_name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
base_url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
is_active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
last_heartbeat: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
heartbeat_interval: {
|
||||||
|
type: Number,
|
||||||
|
default: 60
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: Number,
|
||||||
|
default: () => Math.floor(Date.now() / 1000)
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: Number,
|
||||||
|
default: () => Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
collection: 'jimeng_servers',
|
||||||
|
timestamps: false // 使用自定义时间戳
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建索引
|
||||||
|
JimengServerSchema.index({ server_id: 1 });
|
||||||
|
JimengServerSchema.index({ is_active: 1, last_heartbeat: 1 });
|
||||||
|
|
||||||
|
export default mongoose.model<IJimengServer>('JimengServer', JimengServerSchema);
|
||||||
75
src/lib/database/mongodb.ts
Normal file
75
src/lib/database/mongodb.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import logger from '@/lib/logger.ts';
|
||||||
|
import config from '@/lib/config.ts';
|
||||||
|
|
||||||
|
class MongoDBManager {
|
||||||
|
private static instance: MongoDBManager;
|
||||||
|
private isConnected: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): MongoDBManager {
|
||||||
|
if (!MongoDBManager.instance) {
|
||||||
|
MongoDBManager.instance = new MongoDBManager();
|
||||||
|
}
|
||||||
|
return MongoDBManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const mongoUrl = process.env.MONGODB_URL || config.database?.mongodb?.url || 'mongodb://localhost:27017/jimeng-api';
|
||||||
|
|
||||||
|
await mongoose.connect(mongoUrl, {
|
||||||
|
maxPoolSize: 10,
|
||||||
|
serverSelectionTimeoutMS: 5000,
|
||||||
|
socketTimeoutMS: 45000,
|
||||||
|
bufferCommands: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isConnected = true;
|
||||||
|
logger.success('MongoDB connected successfully');
|
||||||
|
|
||||||
|
// 监听连接事件
|
||||||
|
mongoose.connection.on('error', (err) => {
|
||||||
|
logger.error('MongoDB connection error:', err);
|
||||||
|
this.isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
mongoose.connection.on('disconnected', () => {
|
||||||
|
logger.warn('MongoDB disconnected');
|
||||||
|
this.isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
mongoose.connection.on('reconnected', () => {
|
||||||
|
logger.success('MongoDB reconnected');
|
||||||
|
this.isConnected = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('MongoDB connection failed:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await mongoose.disconnect();
|
||||||
|
this.isConnected = false;
|
||||||
|
logger.info('MongoDB disconnected');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('MongoDB disconnect error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isMongoConnected(): boolean {
|
||||||
|
return this.isConnected && mongoose.connection.readyState === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConnection() {
|
||||||
|
return mongoose.connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MongoDBManager.getInstance();
|
||||||
275
src/lib/services/HeartbeatService.ts
Normal file
275
src/lib/services/HeartbeatService.ts
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import * as cron from 'node-cron';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import JimengServer, { IJimengServer } from '@/lib/database/models/ServiceHeartbeat.ts';
|
||||||
|
import mongoDBManager from '@/lib/database/mongodb.ts';
|
||||||
|
import logger from '@/lib/logger.ts';
|
||||||
|
import config from '@/lib/config.ts';
|
||||||
|
import environment from '@/lib/environment.ts';
|
||||||
|
|
||||||
|
export class HeartbeatService {
|
||||||
|
private static instance: HeartbeatService;
|
||||||
|
private serverId: string;
|
||||||
|
private serverName: string;
|
||||||
|
private baseUrl: string;
|
||||||
|
private heartbeatTask: cron.ScheduledTask | null = null;
|
||||||
|
private isRunning: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// 优先从环境变量获取配置,否则使用默认值
|
||||||
|
this.serverId = process.env.SERVICE_ID || config.service?.name || 'jimeng-free-api';
|
||||||
|
this.serverName = process.env.SERVICE_NAME || this.serverId;
|
||||||
|
this.baseUrl = this.buildBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): HeartbeatService {
|
||||||
|
if (!HeartbeatService.instance) {
|
||||||
|
HeartbeatService.instance = new HeartbeatService();
|
||||||
|
}
|
||||||
|
return HeartbeatService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBaseUrl(): string {
|
||||||
|
const host = process.env.HOST || config.service?.host || '0.0.0.0';
|
||||||
|
const port = process.env.PORT || config.service?.port || 3302;
|
||||||
|
|
||||||
|
// 如果明确指定了 BASE_URL,直接使用
|
||||||
|
if (process.env.BASE_URL) {
|
||||||
|
return process.env.BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 Docker 环境,使用容器名称
|
||||||
|
if (process.env.NODE_ENV === 'production' && process.env.CONTAINER_NAME) {
|
||||||
|
return `http://${process.env.CONTAINER_NAME}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PM2 环境或Node.js直接启动环境
|
||||||
|
let targetHost = host;
|
||||||
|
if (host === '0.0.0.0') {
|
||||||
|
// 尝试获取本机的实际IP地址
|
||||||
|
targetHost = this.getLocalIP() || 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://${targetHost}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLocalIP(): string | null {
|
||||||
|
try {
|
||||||
|
const os = require('os');
|
||||||
|
const interfaces = os.networkInterfaces();
|
||||||
|
|
||||||
|
// 优先查找非回环的IPv4地址
|
||||||
|
for (const name of Object.keys(interfaces)) {
|
||||||
|
for (const iface of interfaces[name]) {
|
||||||
|
if (iface.family === 'IPv4' && !iface.internal) {
|
||||||
|
return iface.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to get local IP:', error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
logger.warn('Heartbeat service is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保MongoDB连接可用
|
||||||
|
if (!mongoDBManager.isMongoConnected()) {
|
||||||
|
logger.warn('MongoDB not connected, skipping heartbeat service');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化或更新服务器信息
|
||||||
|
await this.registerServer();
|
||||||
|
|
||||||
|
// 立即发送第一次心跳
|
||||||
|
await this.sendHeartbeat();
|
||||||
|
|
||||||
|
// 设置定时任务 - 每60秒发送一次心跳(适配 Python 项目的默认间隔)
|
||||||
|
const heartbeatInterval = process.env.HEARTBEAT_INTERVAL || config.heartbeat?.interval || 60;
|
||||||
|
const cronExpression = `*/${heartbeatInterval} * * * * *`; // 每 N 秒
|
||||||
|
|
||||||
|
this.heartbeatTask = cron.schedule(cronExpression, async () => {
|
||||||
|
try {
|
||||||
|
await this.sendHeartbeat();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Heartbeat failed:', error);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
scheduled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.heartbeatTask.start();
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
logger.success(`Heartbeat service started for server: ${this.serverId}`);
|
||||||
|
|
||||||
|
// 监听进程退出事件
|
||||||
|
process.on('SIGINT', () => this.gracefulShutdown());
|
||||||
|
process.on('SIGTERM', () => this.gracefulShutdown());
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start heartbeat service:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatTask) {
|
||||||
|
this.heartbeatTask.stop();
|
||||||
|
this.heartbeatTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记服务为非活跃状态
|
||||||
|
await this.markInactive();
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
logger.info('Heartbeat service stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async registerServer(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const serverData = {
|
||||||
|
server_id: this.serverId,
|
||||||
|
server_name: this.serverName,
|
||||||
|
base_url: this.baseUrl,
|
||||||
|
is_active: true,
|
||||||
|
last_heartbeat: currentTime,
|
||||||
|
heartbeat_interval: parseInt(process.env.HEARTBEAT_INTERVAL || '60'),
|
||||||
|
updated_at: currentTime
|
||||||
|
};
|
||||||
|
|
||||||
|
await JimengServer.findOneAndUpdate(
|
||||||
|
{ server_id: this.serverId },
|
||||||
|
{
|
||||||
|
...serverData,
|
||||||
|
$setOnInsert: { created_at: currentTime }
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Server registered: ${this.serverId} at ${this.baseUrl}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to register server:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendHeartbeat(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
await JimengServer.findOneAndUpdate(
|
||||||
|
{ server_id: this.serverId },
|
||||||
|
{
|
||||||
|
last_heartbeat: currentTime,
|
||||||
|
updated_at: currentTime,
|
||||||
|
is_active: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`Heartbeat sent for server ${this.serverId}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send heartbeat:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markInactive(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
await JimengServer.findOneAndUpdate(
|
||||||
|
{ server_id: this.serverId },
|
||||||
|
{
|
||||||
|
is_active: false,
|
||||||
|
updated_at: currentTime
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Server ${this.serverId} marked as inactive`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to mark server as inactive:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async gracefulShutdown(): Promise<void> {
|
||||||
|
logger.info('Received shutdown signal, stopping heartbeat service...');
|
||||||
|
await this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getServerInfo() {
|
||||||
|
return {
|
||||||
|
serverId: this.serverId,
|
||||||
|
serverName: this.serverName,
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
isRunning: this.isRunning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有活跃服务器
|
||||||
|
public static async getActiveServers(): Promise<IJimengServer[]> {
|
||||||
|
try {
|
||||||
|
return await JimengServer.find({
|
||||||
|
is_active: true
|
||||||
|
}).sort({ last_heartbeat: -1 });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get active servers:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取在线服务器(基于心跳超时检查)
|
||||||
|
public static async getOnlineServers(): Promise<IJimengServer[]> {
|
||||||
|
try {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const timeoutFactor = 1.5; // 超时倍数
|
||||||
|
|
||||||
|
const servers = await JimengServer.find({ is_active: true });
|
||||||
|
|
||||||
|
return servers.filter(server => {
|
||||||
|
const heartbeatTimeout = server.heartbeat_interval * timeoutFactor;
|
||||||
|
return (currentTime - server.last_heartbeat) <= heartbeatTimeout;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get online servers:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理离线服务器记录
|
||||||
|
public static async cleanupOfflineServers(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const cleanupTimeout = 24 * 60 * 60; // 24小时
|
||||||
|
|
||||||
|
await JimengServer.deleteMany({
|
||||||
|
$or: [
|
||||||
|
{ is_active: false },
|
||||||
|
{ last_heartbeat: { $lt: currentTime - cleanupTimeout } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('Cleaned up offline servers');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to cleanup offline servers:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeartbeatService.getInstance();
|
||||||
239
src/lib/tos/TOS_README.md
Normal file
239
src/lib/tos/TOS_README.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# TOS (火山对象存储) Node.js SDK 集成
|
||||||
|
|
||||||
|
## 📦 安装依赖
|
||||||
|
|
||||||
|
在您的 jimeng-free-api 项目中安装以下依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装火山对象存储 JS SDK
|
||||||
|
npm install @volcengine/tos-sdk
|
||||||
|
|
||||||
|
# 安装其他必需的依赖
|
||||||
|
npm install axios mime-types
|
||||||
|
|
||||||
|
# 安装 TypeScript 类型定义
|
||||||
|
npm install --save-dev @types/node @types/mime-types typescript
|
||||||
|
|
||||||
|
# 可选:安装开发工具
|
||||||
|
npm install --save-dev ts-node ts-node-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
将以下文件复制到您的 `src/lib/` 目录中:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/lib/
|
||||||
|
├── util.ts # 现有的工具文件
|
||||||
|
├── tos-client.ts # TOS 客户端封装类
|
||||||
|
├── tos-service.ts # TOS 服务类和使用示例
|
||||||
|
└── tos-test.ts # TOS 功能测试文件(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
在您的 `.env` 文件中添加以下配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 火山对象存储配置
|
||||||
|
TOS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
TOS_ACCESS_KEY_SECRET=your_access_key_secret
|
||||||
|
TOS_BUCKET_NAME=your_bucket_name
|
||||||
|
TOS_SELF_DOMAIN=your_custom_domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 基础配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TOSClientWrapper, TOSClientConfig } from './tos-client';
|
||||||
|
|
||||||
|
const config: TOSClientConfig = {
|
||||||
|
accessKeyId: process.env.TOS_ACCESS_KEY_ID!,
|
||||||
|
accessKeySecret: process.env.TOS_ACCESS_KEY_SECRET!,
|
||||||
|
endpoint: 'tos-cn-beijing.volces.com',
|
||||||
|
region: 'cn-beijing',
|
||||||
|
bucketName: process.env.TOS_BUCKET_NAME!,
|
||||||
|
selfDomain: process.env.TOS_SELF_DOMAIN!
|
||||||
|
};
|
||||||
|
|
||||||
|
const tosClient = new TOSClientWrapper(config);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 快速使用服务类
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import TOSService from './tos-service';
|
||||||
|
|
||||||
|
// 上传文本
|
||||||
|
const textUrl = await TOSService.uploadText('Hello World', 'test.txt');
|
||||||
|
|
||||||
|
// 上传本地文件
|
||||||
|
const fileUrl = await TOSService.uploadLocalFile('/path/to/file.jpg');
|
||||||
|
|
||||||
|
// 从URL上传
|
||||||
|
const urlResult = await TOSService.uploadFromUrl('https://example.com/image.jpg');
|
||||||
|
|
||||||
|
// 上传Buffer
|
||||||
|
const buffer = Buffer.from('data');
|
||||||
|
const bufferUrl = await TOSService.uploadBuffer(buffer, 'file.txt', 'text/plain');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 直接使用客户端
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TOSClientWrapper } from './tos-client';
|
||||||
|
|
||||||
|
const client = new TOSClientWrapper(config);
|
||||||
|
|
||||||
|
// 上传字符串
|
||||||
|
await client.uploadString('content', 'path/file.txt');
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
await client.uploadFile('./local-file.jpg', 'uploads/image.jpg');
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const data = await client.downloadFile('uploads/image.jpg');
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
await client.deleteFile('uploads/image.jpg');
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 对照表
|
||||||
|
|
||||||
|
以下是 Python 版本和 TypeScript 版本的 API 对照:
|
||||||
|
|
||||||
|
| Python 方法 | TypeScript 方法 | 说明 |
|
||||||
|
|-------------|----------------|------|
|
||||||
|
| `upload_string()` | `uploadString()` | 上传字符串内容 |
|
||||||
|
| `upload_file()` | `uploadFile()` | 上传本地文件 |
|
||||||
|
| `upload_bytes()` | `uploadBytes()` | 上传字节数据 |
|
||||||
|
| `upload_from_url()` | `uploadFromUrl()` | 从URL上传文件 |
|
||||||
|
| `download_file()` | `downloadFile()` | 下载文件 |
|
||||||
|
| `delete_file()` | `deleteFile()` | 删除文件 |
|
||||||
|
| `generate_url()` | `generateUrl()` | 生成签名URL |
|
||||||
|
| `get_base_url()` | `getBaseUrl()` | 获取基础URL |
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
✅ 支持字符串上传
|
||||||
|
✅ 支持本地文件上传
|
||||||
|
✅ 支持从URL上传文件
|
||||||
|
✅ 支持Buffer数据上传
|
||||||
|
✅ 支持文件下载
|
||||||
|
✅ 支持文件删除
|
||||||
|
✅ 支持生成签名URL
|
||||||
|
✅ 支持检查文件存在性
|
||||||
|
✅ 支持获取文件信息
|
||||||
|
✅ 支持列出存储桶对象
|
||||||
|
✅ 自动Content-Type检测
|
||||||
|
✅ 错误处理和重试机制
|
||||||
|
✅ TypeScript 类型支持
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保您的火山对象存储账户已正确配置
|
||||||
|
2. 自定义域名需要在火山控制台中配置
|
||||||
|
3. 生产环境中请妥善保管访问密钥
|
||||||
|
4. 建议在生产环境中启用HTTPS
|
||||||
|
5. 大文件上传建议使用分片上传(可根据需要扩展)
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### 问题 1:找不到模块 '@volcengine/tos-sdk'
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```bash
|
||||||
|
npm install @volcengine/tos-sdk
|
||||||
|
# 或
|
||||||
|
yarn add @volcengine/tos-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 2:找不到名称 'process' 或 'Buffer'
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```bash
|
||||||
|
npm install --save-dev @types/node
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 3:编译错误
|
||||||
|
|
||||||
|
**检查 tsconfig.json 配置:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"types": ["node"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 4:SSL 警告
|
||||||
|
|
||||||
|
**如果遇到 SSL 警告,可以:**
|
||||||
|
1. 设置 `disableSSLWarnings: true`
|
||||||
|
2. 或者使用正确的 SSL 证书
|
||||||
|
|
||||||
|
### 问题 5:上传失败
|
||||||
|
|
||||||
|
**检查以下配置:**
|
||||||
|
1. AccessKey 和 SecretKey 是否正确
|
||||||
|
2. Bucket 名称是否正确
|
||||||
|
3. Region 是否匹配
|
||||||
|
4. 网络连接是否正常
|
||||||
|
5. 权限是否足够
|
||||||
|
|
||||||
|
### 问题 6:自定义域名访问失败
|
||||||
|
|
||||||
|
**检查:**
|
||||||
|
1. 域名 DNS 解析是否正确
|
||||||
|
2. 域名是否已在火山控制台绑定
|
||||||
|
3. HTTPS 证书是否有效
|
||||||
|
|
||||||
|
## 🎆 快速测试
|
||||||
|
|
||||||
|
**创建测试文件:**
|
||||||
|
```typescript
|
||||||
|
// test-quick.ts
|
||||||
|
import TOSService from './tos-service';
|
||||||
|
|
||||||
|
async function quickTest() {
|
||||||
|
try {
|
||||||
|
// 测试上传文本
|
||||||
|
const result = await TOSService.uploadText('你好,TOS!', 'hello.txt');
|
||||||
|
console.log('上传成功:', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试失败:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quickTest();
|
||||||
|
```
|
||||||
|
|
||||||
|
**运行测试:**
|
||||||
|
```bash
|
||||||
|
# 使用 ts-node 直接运行
|
||||||
|
ts-node test-quick.ts
|
||||||
|
|
||||||
|
# 或编译后运行
|
||||||
|
tsc test-quick.ts
|
||||||
|
node test-quick.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
所有方法都包含完善的错误处理,会抛出带有详细信息的错误:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const result = await TOSService.uploadFile('/path/to/file.jpg');
|
||||||
|
console.log('上传成功:', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
360
src/lib/tos/tos-client.ts
Normal file
360
src/lib/tos/tos-client.ts
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
// 注意: 使用前请先安装依赖: npm install @volcengine/tos-sdk mime-types axios @types/node
|
||||||
|
import { TosClient } from '@volcengine/tos-sdk';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { lookup as mimeLookup } from 'mime-types';
|
||||||
|
|
||||||
|
export interface TOSClientConfig {
|
||||||
|
accessKeyId: string;
|
||||||
|
accessKeySecret: string;
|
||||||
|
endpoint: string;
|
||||||
|
region: string;
|
||||||
|
bucketName: string;
|
||||||
|
selfDomain: string;
|
||||||
|
disableSSLWarnings?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadOptions {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
returnUrl?: boolean;
|
||||||
|
expires?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TOSClientWrapper {
|
||||||
|
private client: TosClient;
|
||||||
|
private bucketName: string;
|
||||||
|
private selfDomain: string;
|
||||||
|
private endpoint: string;
|
||||||
|
|
||||||
|
constructor(config: TOSClientConfig) {
|
||||||
|
const {
|
||||||
|
accessKeyId,
|
||||||
|
accessKeySecret,
|
||||||
|
endpoint,
|
||||||
|
region,
|
||||||
|
bucketName,
|
||||||
|
selfDomain,
|
||||||
|
disableSSLWarnings = true
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
this.bucketName = bucketName;
|
||||||
|
this.selfDomain = selfDomain;
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
|
||||||
|
// 创建 TOS 客户端
|
||||||
|
this.client = new TosClient({
|
||||||
|
accessKeyId,
|
||||||
|
accessKeySecret,
|
||||||
|
endpoint: selfDomain, // 使用自定义域名作为 endpoint
|
||||||
|
region,
|
||||||
|
isCustomDomain: true, // 启用自定义域名
|
||||||
|
connectionTimeout: 30000,
|
||||||
|
requestTimeout: 60000,
|
||||||
|
maxRetryCount: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
// 禁用 SSL 警告(如果需要)
|
||||||
|
if (disableSSLWarnings && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0') {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础URL(不带签名参数)
|
||||||
|
*/
|
||||||
|
getBaseUrl(objectKey: string): string {
|
||||||
|
return `https://${this.selfDomain}/${objectKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成带签名的临时访问URL
|
||||||
|
*/
|
||||||
|
async generateUrl(objectKey: string, expires: number = 3600): Promise<string> {
|
||||||
|
try {
|
||||||
|
const result = this.client.getPreSignedUrl({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: objectKey,
|
||||||
|
method: 'GET',
|
||||||
|
expires
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`生成签名URL失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传字符串内容
|
||||||
|
*/
|
||||||
|
async uploadString(
|
||||||
|
contentStr: string,
|
||||||
|
objectKey: string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const { headers = {}, returnUrl = true } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.client.putObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: objectKey,
|
||||||
|
body: Buffer.from(contentStr, 'utf-8'),
|
||||||
|
contentType: 'text/plain',
|
||||||
|
...headers
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`upload_string http status code: ${result.statusCode}`);
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(`上传失败,HTTP状态码: ${result.statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnUrl ? this.getBaseUrl(objectKey) : objectKey;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`上传字符串到TOS失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传本地文件
|
||||||
|
*/
|
||||||
|
async uploadFile(
|
||||||
|
localFilePath: string,
|
||||||
|
objectKey?: string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const { headers = {}, returnUrl = true } = options;
|
||||||
|
|
||||||
|
if (!fs.existsSync(localFilePath)) {
|
||||||
|
throw new Error(`本地文件不存在: ${localFilePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定 object_key,则使用文件名
|
||||||
|
if (!objectKey) {
|
||||||
|
objectKey = path.basename(localFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动设置 Content-Type
|
||||||
|
const contentType = mimeLookup(localFilePath) || 'application/octet-stream';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffer = fs.readFileSync(localFilePath);
|
||||||
|
|
||||||
|
const result = await this.client.putObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: objectKey,
|
||||||
|
body: fileBuffer,
|
||||||
|
contentType,
|
||||||
|
...headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(`上传失败,HTTP状态码: ${result.statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnUrl ? this.getBaseUrl(objectKey) : objectKey;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`上传文件到TOS失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传字节数据
|
||||||
|
*/
|
||||||
|
async uploadBytes(
|
||||||
|
data: Buffer,
|
||||||
|
objectKey: string,
|
||||||
|
contentType?: string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const { headers = {}, returnUrl = true } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.client.putObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: objectKey,
|
||||||
|
body: data,
|
||||||
|
contentType: contentType || 'application/octet-stream',
|
||||||
|
...headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(`上传失败,HTTP状态码: ${result.statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnUrl ? this.getBaseUrl(objectKey) : objectKey;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`上传字节数据到TOS失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从网络URL下载文件并上传到TOS
|
||||||
|
*/
|
||||||
|
async uploadFromUrl(
|
||||||
|
url: string,
|
||||||
|
objectKey: string,
|
||||||
|
options: UploadOptions & { timeout?: number } = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const { headers = {}, timeout = 30000, returnUrl = true } = options;
|
||||||
|
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
throw new Error('URL必须以http://或https://开头');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 下载文件
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取内容类型
|
||||||
|
let contentType = response.headers['content-type'] || '';
|
||||||
|
if (!contentType) {
|
||||||
|
contentType = mimeLookup(url) || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到TOS
|
||||||
|
return this.uploadBytes(
|
||||||
|
Buffer.from(response.data),
|
||||||
|
objectKey,
|
||||||
|
contentType,
|
||||||
|
{ headers, returnUrl }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||||
|
throw new Error(`下载网络文件超时: ${url}`);
|
||||||
|
}
|
||||||
|
throw new Error(`上传网络文件到TOS失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化TOS对象键(路径)
|
||||||
|
*/
|
||||||
|
private formatObjectKey(objectKey: string): string {
|
||||||
|
// 如果 objectKey 包含 selfDomain,截取 selfDomain 后面的字符作为新的 objectKey
|
||||||
|
if (this.selfDomain && objectKey.includes(this.selfDomain)) {
|
||||||
|
const domainIndex = objectKey.indexOf(this.selfDomain);
|
||||||
|
if (domainIndex !== -1) {
|
||||||
|
// 截取 selfDomain 后面的部分,去掉开头的斜杠
|
||||||
|
objectKey = objectKey.substring(domainIndex + this.selfDomain.length).replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
*/
|
||||||
|
async deleteFile(objectKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const formattedKey = this.formatObjectKey(objectKey);
|
||||||
|
|
||||||
|
await this.client.deleteObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: formattedKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`删除文件失败: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从TOS下载文件并返回文件数据
|
||||||
|
*/
|
||||||
|
async downloadFile(objectKey: string): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
const formattedKey = this.formatObjectKey(objectKey);
|
||||||
|
|
||||||
|
const result = await this.client.getObjectV2({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: formattedKey,
|
||||||
|
dataType: 'buffer'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.content) {
|
||||||
|
throw new Error(`文件内容为空: ${objectKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getObjectV2 with dataType: 'buffer' 直接返回 Buffer
|
||||||
|
return result.data.content as Buffer;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NoSuchKey') {
|
||||||
|
throw new Error(`文件不存在: ${objectKey}`);
|
||||||
|
}
|
||||||
|
throw new Error(`下载文件失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
*/
|
||||||
|
async fileExists(objectKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const formattedKey = this.formatObjectKey(objectKey);
|
||||||
|
|
||||||
|
await this.client.headObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: formattedKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NotFound' || error.code === 'NoSuchKey') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new Error(`检查文件存在性失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件信息
|
||||||
|
*/
|
||||||
|
async getFileInfo(objectKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const formattedKey = this.formatObjectKey(objectKey);
|
||||||
|
|
||||||
|
const result = await this.client.headObject({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
key: formattedKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentLength: result.data['content-length'],
|
||||||
|
contentType: result.data['content-type'],
|
||||||
|
lastModified: result.data['last-modified'],
|
||||||
|
etag: result.data.etag
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NotFound' || error.code === 'NoSuchKey') {
|
||||||
|
throw new Error(`文件不存在: ${objectKey}`);
|
||||||
|
}
|
||||||
|
throw new Error(`获取文件信息失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出存储桶中的对象
|
||||||
|
*/
|
||||||
|
async listObjects(prefix?: string, maxKeys: number = 1000): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.client.listObjects({
|
||||||
|
bucket: this.bucketName,
|
||||||
|
prefix,
|
||||||
|
maxKeys
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data.Contents || [];
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`列出对象失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TOSClientWrapper;
|
||||||
201
src/lib/tos/tos-service.ts
Normal file
201
src/lib/tos/tos-service.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { TOSClientWrapper, TOSClientConfig } from './tos-client.js';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// TOS 客户端配置
|
||||||
|
const TOS_CONFIG: TOSClientConfig = {
|
||||||
|
accessKeyId: process.env.TOS_ACCESS_KEY_ID || 'your-access-key-id',
|
||||||
|
accessKeySecret: process.env.TOS_ACCESS_KEY_SECRET || 'your-access-key-secret',
|
||||||
|
endpoint: process.env.TOS_ENDPOINT || 'tos-cn-beijing.volces.com', // 火山对象存储端点
|
||||||
|
region: process.env.TOS_REGION || 'cn-beijing',
|
||||||
|
bucketName: process.env.TOS_BUCKET_NAME || 'your-bucket-name',
|
||||||
|
selfDomain: process.env.TOS_SELF_DOMAIN || 'your-domain.com', // 自定义域名
|
||||||
|
disableSSLWarnings: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 TOS 客户端实例
|
||||||
|
const tosClient = new TOSClientWrapper(TOS_CONFIG);
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
export class TOSService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文本内容
|
||||||
|
*/
|
||||||
|
static async uploadText(content: string, fileName: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const objectKey = `text/${Date.now()}-${fileName}`;
|
||||||
|
return await tosClient.uploadString(content, objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传文本失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传本地文件
|
||||||
|
*/
|
||||||
|
static async uploadLocalFile(filePath: string, targetPath?: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const objectKey = targetPath || `files/${Date.now()}-${path.basename(filePath)}`;
|
||||||
|
return await tosClient.uploadFile(filePath, objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传本地文件失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URL上传文件
|
||||||
|
*/
|
||||||
|
static async uploadFromUrl(url: string, targetPath?: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const fileName = path.basename(new URL(url).pathname) || 'downloaded-file';
|
||||||
|
const objectKey = targetPath || `downloads/${Date.now()}-${fileName}`;
|
||||||
|
return await tosClient.uploadFromUrl(url, objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从URL上传文件失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传Buffer数据
|
||||||
|
*/
|
||||||
|
static async uploadBuffer(
|
||||||
|
buffer: Buffer,
|
||||||
|
fileName: string,
|
||||||
|
contentType?: string,
|
||||||
|
folder: string = 'uploads'
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const objectKey = `${folder}/${Date.now()}-${fileName}`;
|
||||||
|
return await tosClient.uploadBytes(buffer, objectKey, contentType);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传Buffer失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
*/
|
||||||
|
static async downloadFile(objectKey: string): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
return await tosClient.downloadFile(objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载文件失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
*/
|
||||||
|
static async deleteFile(objectKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await tosClient.deleteFile(objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除文件失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
*/
|
||||||
|
static async fileExists(objectKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await tosClient.fileExists(objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查文件存在性失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件信息
|
||||||
|
*/
|
||||||
|
static async getFileInfo(objectKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await tosClient.getFileInfo(objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件信息失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成临时访问URL
|
||||||
|
*/
|
||||||
|
static async generateTempUrl(objectKey: string, expires: number = 3600): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await tosClient.generateUrl(objectKey, expires);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成临时URL失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公开访问URL
|
||||||
|
*/
|
||||||
|
static getPublicUrl(objectKey: string): string {
|
||||||
|
return tosClient.getBaseUrl(objectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出文件
|
||||||
|
*/
|
||||||
|
static async listFiles(prefix?: string, maxKeys: number = 100): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
return await tosClient.listObjects(prefix, maxKeys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('列出文件失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出客户端实例和服务类
|
||||||
|
export { tosClient, TOSClientWrapper };
|
||||||
|
export default TOSService;
|
||||||
|
|
||||||
|
// 使用示例代码
|
||||||
|
/*
|
||||||
|
// 1. 上传文本
|
||||||
|
const textUrl = await TOSService.uploadText('Hello World', 'test.txt');
|
||||||
|
console.log('文本上传成功:', textUrl);
|
||||||
|
|
||||||
|
// 2. 上传本地文件
|
||||||
|
const fileUrl = await TOSService.uploadLocalFile('/path/to/local/file.jpg');
|
||||||
|
console.log('文件上传成功:', fileUrl);
|
||||||
|
|
||||||
|
// 3. 从URL上传
|
||||||
|
const urlUploadResult = await TOSService.uploadFromUrl('https://example.com/image.jpg');
|
||||||
|
console.log('URL上传成功:', urlUploadResult);
|
||||||
|
|
||||||
|
// 4. 上传Buffer
|
||||||
|
const buffer = Buffer.from('Hello Buffer');
|
||||||
|
const bufferUrl = await TOSService.uploadBuffer(buffer, 'buffer-file.txt', 'text/plain');
|
||||||
|
console.log('Buffer上传成功:', bufferUrl);
|
||||||
|
|
||||||
|
// 5. 下载文件
|
||||||
|
const downloadedData = await TOSService.downloadFile('uploads/file-key');
|
||||||
|
console.log('文件下载成功, 大小:', downloadedData.length);
|
||||||
|
|
||||||
|
// 6. 检查文件存在性
|
||||||
|
const exists = await TOSService.fileExists('uploads/file-key');
|
||||||
|
console.log('文件是否存在:', exists);
|
||||||
|
|
||||||
|
// 7. 删除文件
|
||||||
|
const deleted = await TOSService.deleteFile('uploads/file-key');
|
||||||
|
console.log('文件删除结果:', deleted);
|
||||||
|
|
||||||
|
// 8. 生成临时URL
|
||||||
|
const tempUrl = await TOSService.generateTempUrl('uploads/file-key', 3600);
|
||||||
|
console.log('临时URL:', tempUrl);
|
||||||
|
|
||||||
|
// 9. 获取公开URL
|
||||||
|
const publicUrl = TOSService.getPublicUrl('uploads/file-key');
|
||||||
|
console.log('公开URL:', publicUrl);
|
||||||
|
*/
|
||||||
310
src/lib/tos/tos-test.ts
Normal file
310
src/lib/tos/tos-test.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { TOSClientWrapper, TOSClientConfig } from './tos-client.js';
|
||||||
|
import TOSService from './tos-service.js';
|
||||||
|
|
||||||
|
// 测试配置 - 请根据实际情况修改
|
||||||
|
const TEST_CONFIG: TOSClientConfig = {
|
||||||
|
accessKeyId: process.env.TOS_ACCESS_KEY_ID || 'test-access-key',
|
||||||
|
accessKeySecret: process.env.TOS_ACCESS_KEY_SECRET || 'test-secret-key',
|
||||||
|
endpoint: 'tos-cn-beijing.volces.com',
|
||||||
|
region: 'cn-beijing',
|
||||||
|
bucketName: process.env.TOS_BUCKET_NAME || 'test-bucket',
|
||||||
|
selfDomain: process.env.TOS_SELF_DOMAIN || 'test-domain.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
class TOSClientTest {
|
||||||
|
private tosClient: TOSClientWrapper;
|
||||||
|
private testResults: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tosClient = new TOSClientWrapper(TEST_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试文件
|
||||||
|
*/
|
||||||
|
private createTestFile(): string {
|
||||||
|
const testDir = path.join(process.cwd(), 'test-files');
|
||||||
|
if (!fs.existsSync(testDir)) {
|
||||||
|
fs.mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFilePath = path.join(testDir, 'test-image.txt');
|
||||||
|
const testContent = `测试文件内容\n创建时间: ${new Date().toISOString()}\n这是一个用于测试TOS上传功能的文件。`;
|
||||||
|
|
||||||
|
fs.writeFileSync(testFilePath, testContent, 'utf-8');
|
||||||
|
return testFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试字符串上传
|
||||||
|
*/
|
||||||
|
async testUploadString(): Promise<string | null> {
|
||||||
|
console.log('\n🧪 测试字符串上传...');
|
||||||
|
try {
|
||||||
|
const testContent = `测试字符串内容 - ${Date.now()}`;
|
||||||
|
const objectKey = `test/string-${Date.now()}.txt`;
|
||||||
|
|
||||||
|
const url = await this.tosClient.uploadString(testContent, objectKey);
|
||||||
|
console.log('✅ 字符串上传成功:', url);
|
||||||
|
|
||||||
|
// 验证文件是否存在
|
||||||
|
const exists = await this.tosClient.fileExists(objectKey);
|
||||||
|
console.log('✅ 文件存在性验证:', exists);
|
||||||
|
|
||||||
|
this.testResults['uploadString'] = true;
|
||||||
|
return objectKey;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 字符串上传失败:', error.message);
|
||||||
|
this.testResults['uploadString'] = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试本地文件上传
|
||||||
|
*/
|
||||||
|
async testUploadFile(): Promise<string | null> {
|
||||||
|
console.log('\n🧪 测试本地文件上传...');
|
||||||
|
try {
|
||||||
|
const testFilePath = this.createTestFile();
|
||||||
|
const objectKey = `test/file-${Date.now()}.txt`;
|
||||||
|
|
||||||
|
const url = await this.tosClient.uploadFile(testFilePath, objectKey);
|
||||||
|
console.log('✅ 本地文件上传成功:', url);
|
||||||
|
|
||||||
|
// 清理测试文件
|
||||||
|
fs.unlinkSync(testFilePath);
|
||||||
|
fs.rmdirSync(path.dirname(testFilePath));
|
||||||
|
|
||||||
|
this.testResults['uploadFile'] = true;
|
||||||
|
return objectKey;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 本地文件上传失败:', error.message);
|
||||||
|
this.testResults['uploadFile'] = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Buffer上传
|
||||||
|
*/
|
||||||
|
async testUploadBuffer(): Promise<string | null> {
|
||||||
|
console.log('\n🧪 测试Buffer上传...');
|
||||||
|
try {
|
||||||
|
const testBuffer = Buffer.from(`测试Buffer内容 - ${Date.now()}`, 'utf-8');
|
||||||
|
const objectKey = `test/buffer-${Date.now()}.txt`;
|
||||||
|
|
||||||
|
const url = await this.tosClient.uploadBytes(testBuffer, objectKey, 'text/plain');
|
||||||
|
console.log('✅ Buffer上传成功:', url);
|
||||||
|
|
||||||
|
this.testResults['uploadBuffer'] = true;
|
||||||
|
return objectKey;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Buffer上传失败:', error.message);
|
||||||
|
this.testResults['uploadBuffer'] = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试文件下载
|
||||||
|
*/
|
||||||
|
async testDownloadFile(objectKey: string): Promise<void> {
|
||||||
|
if (!objectKey) {
|
||||||
|
console.log('\n⏭️ 跳过文件下载测试 (没有可用的测试文件)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🧪 测试文件下载...');
|
||||||
|
try {
|
||||||
|
const data = await this.tosClient.downloadFile(objectKey);
|
||||||
|
console.log('✅ 文件下载成功, 大小:', data.length, '字节');
|
||||||
|
console.log('📄 文件内容预览:', data.toString('utf-8').substring(0, 100) + '...');
|
||||||
|
|
||||||
|
this.testResults['downloadFile'] = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 文件下载失败:', error.message);
|
||||||
|
this.testResults['downloadFile'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试文件信息获取
|
||||||
|
*/
|
||||||
|
async testGetFileInfo(objectKey: string): Promise<void> {
|
||||||
|
if (!objectKey) {
|
||||||
|
console.log('\n⏭️ 跳过文件信息测试 (没有可用的测试文件)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🧪 测试获取文件信息...');
|
||||||
|
try {
|
||||||
|
const info = await this.tosClient.getFileInfo(objectKey);
|
||||||
|
console.log('✅ 文件信息获取成功:', info);
|
||||||
|
|
||||||
|
this.testResults['getFileInfo'] = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 获取文件信息失败:', error.message);
|
||||||
|
this.testResults['getFileInfo'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试生成签名URL
|
||||||
|
*/
|
||||||
|
async testGenerateUrl(objectKey: string): Promise<void> {
|
||||||
|
if (!objectKey) {
|
||||||
|
console.log('\n⏭️ 跳过URL生成测试 (没有可用的测试文件)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🧪 测试生成签名URL...');
|
||||||
|
try {
|
||||||
|
const signedUrl = await this.tosClient.generateUrl(objectKey, 3600);
|
||||||
|
console.log('✅ 签名URL生成成功:', signedUrl.substring(0, 100) + '...');
|
||||||
|
|
||||||
|
const publicUrl = this.tosClient.getBaseUrl(objectKey);
|
||||||
|
console.log('✅ 公开URL生成成功:', publicUrl);
|
||||||
|
|
||||||
|
this.testResults['generateUrl'] = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ URL生成失败:', error.message);
|
||||||
|
this.testResults['generateUrl'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试服务类方法
|
||||||
|
*/
|
||||||
|
async testTOSService(): Promise<void> {
|
||||||
|
console.log('\n🧪 测试TOS服务类...');
|
||||||
|
try {
|
||||||
|
// 测试上传文本
|
||||||
|
const textUrl = await TOSService.uploadText('服务类测试内容', 'service-test.txt');
|
||||||
|
console.log('✅ 服务类文本上传成功:', textUrl);
|
||||||
|
|
||||||
|
// 测试上传Buffer
|
||||||
|
const buffer = Buffer.from('服务类Buffer测试', 'utf-8');
|
||||||
|
const bufferUrl = await TOSService.uploadBuffer(buffer, 'service-buffer-test.txt', 'text/plain');
|
||||||
|
console.log('✅ 服务类Buffer上传成功:', bufferUrl);
|
||||||
|
|
||||||
|
this.testResults['tosService'] = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ TOS服务类测试失败:', error.message);
|
||||||
|
this.testResults['tosService'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试文件删除(清理测试文件)
|
||||||
|
*/
|
||||||
|
async testDeleteFile(objectKey: string): Promise<void> {
|
||||||
|
if (!objectKey) {
|
||||||
|
console.log('\n⏭️ 跳过文件删除测试 (没有可用的测试文件)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🧪 测试文件删除...');
|
||||||
|
try {
|
||||||
|
const deleted = await this.tosClient.deleteFile(objectKey);
|
||||||
|
console.log('✅ 文件删除成功:', deleted);
|
||||||
|
|
||||||
|
// 验证文件是否已被删除
|
||||||
|
const exists = await this.tosClient.fileExists(objectKey);
|
||||||
|
console.log('✅ 删除验证 (应为false):', exists);
|
||||||
|
|
||||||
|
this.testResults['deleteFile'] = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 文件删除失败:', error.message);
|
||||||
|
this.testResults['deleteFile'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行所有测试
|
||||||
|
*/
|
||||||
|
async runAllTests(): Promise<void> {
|
||||||
|
console.log('🚀 开始TOS客户端功能测试...');
|
||||||
|
console.log('📋 测试配置:');
|
||||||
|
console.log(' - Endpoint:', TEST_CONFIG.endpoint);
|
||||||
|
console.log(' - Region:', TEST_CONFIG.region);
|
||||||
|
console.log(' - Bucket:', TEST_CONFIG.bucketName);
|
||||||
|
console.log(' - Domain:', TEST_CONFIG.selfDomain);
|
||||||
|
|
||||||
|
let testObjectKey: string | null = null;
|
||||||
|
|
||||||
|
// 执行各项测试
|
||||||
|
testObjectKey = await this.testUploadString();
|
||||||
|
await this.testUploadFile();
|
||||||
|
const bufferObjectKey = await this.testUploadBuffer();
|
||||||
|
|
||||||
|
// 使用上传的文件进行其他测试
|
||||||
|
if (testObjectKey) {
|
||||||
|
await this.testDownloadFile(testObjectKey);
|
||||||
|
await this.testGetFileInfo(testObjectKey);
|
||||||
|
await this.testGenerateUrl(testObjectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试服务类
|
||||||
|
await this.testTOSService();
|
||||||
|
|
||||||
|
// 清理测试文件
|
||||||
|
if (testObjectKey) {
|
||||||
|
await this.testDeleteFile(testObjectKey);
|
||||||
|
}
|
||||||
|
if (bufferObjectKey) {
|
||||||
|
await this.testDeleteFile(bufferObjectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出测试结果
|
||||||
|
this.printTestResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印测试结果
|
||||||
|
*/
|
||||||
|
private printTestResults(): void {
|
||||||
|
console.log('\n📊 测试结果汇总:');
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
|
||||||
|
let passCount = 0;
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
Object.entries(this.testResults).forEach(([testName, result]) => {
|
||||||
|
totalCount++;
|
||||||
|
if (result) {
|
||||||
|
passCount++;
|
||||||
|
console.log(`✅ ${testName}: 通过`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ ${testName}: 失败`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
console.log(`📈 总体结果: ${passCount}/${totalCount} 测试通过`);
|
||||||
|
|
||||||
|
if (passCount === totalCount) {
|
||||||
|
console.log('🎉 所有测试都通过了!TOS客户端集成成功!');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 部分测试失败,请检查配置和网络连接。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试的函数
|
||||||
|
export async function runTOSTests(): Promise<void> {
|
||||||
|
const tester = new TOSClientTest();
|
||||||
|
await tester.runAllTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果直接运行此文件,则执行测试
|
||||||
|
// if (require.main === module) {
|
||||||
|
// runTOSTests().catch(error => {
|
||||||
|
// console.error('测试执行失败:', error);
|
||||||
|
// process.exit(1);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default TOSClientTest;
|
||||||
371
start-node.sh
Normal file
371
start-node.sh
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 即梦 Free API Node.js 启动脚本
|
||||||
|
# 使用方法: ./start-node.sh [dev|prod|stop|restart|status]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查Node.js环境
|
||||||
|
check_node_env() {
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
log_error "Node.js 未安装,请先安装 Node.js v20.x"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v yarn &> /dev/null; then
|
||||||
|
log_error "Yarn 未安装,请先安装 Yarn"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||||
|
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||||
|
log_warning "建议使用 Node.js v20.x,当前版本: $(node -v)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Node.js 环境检查通过: $(node -v)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查PM2
|
||||||
|
check_pm2() {
|
||||||
|
if ! command -v pm2 &> /dev/null; then
|
||||||
|
log_info "PM2 未安装,正在安装..."
|
||||||
|
npm install -g pm2
|
||||||
|
log_success "PM2 安装完成"
|
||||||
|
else
|
||||||
|
log_success "PM2 已安装: $(pm2 -v)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
build_project() {
|
||||||
|
log_info "构建项目..."
|
||||||
|
yarn install
|
||||||
|
yarn run build
|
||||||
|
log_success "项目构建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 加载环境变量文件
|
||||||
|
load_env_file() {
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
log_info "加载 .env 文件..."
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
log_success "环境变量已从 .env 文件加载"
|
||||||
|
elif [ -f ".env.template" ]; then
|
||||||
|
log_warning "发现 .env.template 文件,请复制为 .env 并配置实际值"
|
||||||
|
log_info "命令: cp .env.template .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证必要的环境变量
|
||||||
|
validate_env() {
|
||||||
|
local missing_vars=()
|
||||||
|
|
||||||
|
# 检查必须的环境变量
|
||||||
|
[ -z "$MONGODB_URL" ] && missing_vars+=("MONGODB_URL")
|
||||||
|
[ -z "$TOS_ACCESS_KEY_ID" ] && missing_vars+=("TOS_ACCESS_KEY_ID")
|
||||||
|
[ -z "$TOS_ACCESS_KEY_SECRET" ] && missing_vars+=("TOS_ACCESS_KEY_SECRET")
|
||||||
|
[ -z "$TOS_BUCKET_NAME" ] && missing_vars+=("TOS_BUCKET_NAME")
|
||||||
|
|
||||||
|
if [ ${#missing_vars[@]} -gt 0 ]; then
|
||||||
|
log_error "缺少必要的环境变量:"
|
||||||
|
for var in "${missing_vars[@]}"; do
|
||||||
|
log_error " - $var"
|
||||||
|
done
|
||||||
|
log_info "请设置这些环境变量或创建 .env 文件"
|
||||||
|
log_info "参考: .env.template"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "环境变量验证通过"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置环境变量 (示例)
|
||||||
|
setup_env() {
|
||||||
|
local env_type=$1
|
||||||
|
|
||||||
|
log_info "设置环境变量..."
|
||||||
|
|
||||||
|
# 先尝试加载 .env 文件
|
||||||
|
load_env_file
|
||||||
|
|
||||||
|
# 基础环境变量 (如果未设置则使用默认值)
|
||||||
|
export NODE_ENV=${NODE_ENV:-${env_type}}
|
||||||
|
export SERVICE_ID=${SERVICE_ID:-"jimeng-api-${env_type}"}
|
||||||
|
export SERVICE_NAME=${SERVICE_NAME:-"jimeng-free-api-${env_type}"}
|
||||||
|
export HOST=${HOST:-"0.0.0.0"}
|
||||||
|
export PORT=${PORT:-"3302"}
|
||||||
|
export TOS_REGION=${TOS_REGION:-"cn-beijing"}
|
||||||
|
export TOS_ENDPOINT=${TOS_ENDPOINT:-"tos-cn-beijing.volces.com"}
|
||||||
|
export HEARTBEAT_ENABLED=${HEARTBEAT_ENABLED:-"true"}
|
||||||
|
export HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-"30"}
|
||||||
|
|
||||||
|
# 验证必要的环境变量
|
||||||
|
if ! validate_env; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "环境变量设置完成"
|
||||||
|
log_info "服务ID: $SERVICE_ID"
|
||||||
|
log_info "MongoDB: ${MONGODB_URL%%\?*}" # 只显示主机部分,隐藏查询参数
|
||||||
|
log_info "TOS Region: $TOS_REGION"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 直接启动 (开发模式)
|
||||||
|
start_dev() {
|
||||||
|
log_info "启动开发模式..."
|
||||||
|
setup_env "development"
|
||||||
|
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
build_project
|
||||||
|
fi
|
||||||
|
|
||||||
|
node dist/index.js
|
||||||
|
}
|
||||||
|
|
||||||
|
# PM2启动 (生产模式)
|
||||||
|
start_pm2() {
|
||||||
|
local env_type=$1
|
||||||
|
|
||||||
|
log_info "使用PM2启动 (${env_type}模式)..."
|
||||||
|
check_pm2
|
||||||
|
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
build_project
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 停止现有进程
|
||||||
|
pm2 delete jimeng-free-api 2>/dev/null || true
|
||||||
|
|
||||||
|
# 启动新进程
|
||||||
|
if [ "$env_type" = "production" ]; then
|
||||||
|
pm2 start ecosystem.config.json --env production
|
||||||
|
else
|
||||||
|
pm2 start ecosystem.config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
pm2 save
|
||||||
|
log_success "PM2启动完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
stop_service() {
|
||||||
|
log_info "停止服务..."
|
||||||
|
|
||||||
|
# 停止PM2进程
|
||||||
|
if command -v pm2 &> /dev/null; then
|
||||||
|
pm2 delete jimeng-free-api 2>/dev/null || true
|
||||||
|
log_success "PM2进程已停止"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 停止Node.js进程
|
||||||
|
pkill -f "node dist/index.js" 2>/dev/null || true
|
||||||
|
log_success "Node.js进程已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
restart_service() {
|
||||||
|
log_info "重启服务..."
|
||||||
|
stop_service
|
||||||
|
sleep 2
|
||||||
|
start_pm2 "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
check_status() {
|
||||||
|
log_info "检查服务状态..."
|
||||||
|
|
||||||
|
echo "=== PM2 进程状态 ==="
|
||||||
|
if command -v pm2 &> /dev/null; then
|
||||||
|
pm2 list
|
||||||
|
else
|
||||||
|
echo "PM2 未安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Node.js 进程状态 ==="
|
||||||
|
ps aux | grep "node dist/index.js" | grep -v grep || echo "未找到 Node.js 进程"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 端口状态 ==="
|
||||||
|
netstat -tlnp 2>/dev/null | grep :3302 || echo "端口 3302 未被占用"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 服务健康检查 ==="
|
||||||
|
if curl -s http://localhost:3302/ping > /dev/null 2>&1; then
|
||||||
|
log_success "API 服务正常运行"
|
||||||
|
|
||||||
|
# 检查服务器信息
|
||||||
|
echo "=== 当前服务器信息 ==="
|
||||||
|
curl -s http://localhost:3302/api/servers/current | jq '.data' 2>/dev/null || curl -s http://localhost:3302/api/servers/current
|
||||||
|
else
|
||||||
|
log_error "API 服务无法访问"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
view_logs() {
|
||||||
|
log_info "查看日志..."
|
||||||
|
|
||||||
|
if command -v pm2 &> /dev/null && pm2 list | grep -q jimeng-free-api; then
|
||||||
|
pm2 logs jimeng-free-api --lines 50
|
||||||
|
else
|
||||||
|
log_warning "PM2未运行,查看本地日志文件..."
|
||||||
|
if [ -f "./logs/combined.log" ]; then
|
||||||
|
tail -n 50 ./logs/combined.log
|
||||||
|
else
|
||||||
|
log_warning "未找到日志文件"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示帮助
|
||||||
|
show_help() {
|
||||||
|
echo "即梦 Free API Node.js 启动脚本"
|
||||||
|
echo
|
||||||
|
echo "使用方法:"
|
||||||
|
echo " $0 <command>"
|
||||||
|
echo
|
||||||
|
echo "命令:"
|
||||||
|
echo " dev 直接启动开发模式 (Node.js)"
|
||||||
|
echo " prod 使用PM2启动生产模式"
|
||||||
|
echo " pm2 使用PM2启动开发模式"
|
||||||
|
echo " stop 停止所有服务"
|
||||||
|
echo " restart 重启PM2服务"
|
||||||
|
echo " status 查看服务状态"
|
||||||
|
echo " logs 查看服务日志"
|
||||||
|
echo " build 仅构建项目"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
echo
|
||||||
|
echo "多实例管理:"
|
||||||
|
echo " ./multi-instance.sh start all production # 启动所有实例 (端口 3302, 3303, 3304)"
|
||||||
|
echo " ./multi-instance.sh start 3302,3303 # 启动指定端口实例"
|
||||||
|
echo " ./multi-instance.sh stop all # 停止所有实例"
|
||||||
|
echo " ./multi-instance.sh status # 查看所有实例状态"
|
||||||
|
echo " ./multi-instance.sh logs 3302 # 查看指定实例日志"
|
||||||
|
echo
|
||||||
|
echo "环境变量配置方式:"
|
||||||
|
echo " 方式1: 使用 .env 文件 (推荐)"
|
||||||
|
echo " cp .env.template .env"
|
||||||
|
echo " # 编辑 .env 文件,填入实际配置"
|
||||||
|
echo " $0 prod"
|
||||||
|
echo
|
||||||
|
echo " 方式2: 直接设置环境变量"
|
||||||
|
echo " export MONGODB_URL='mongodb://localhost:27017/jimeng-api'"
|
||||||
|
echo " export TOS_ACCESS_KEY_ID='your_key_id'"
|
||||||
|
echo " # ... 其他变量"
|
||||||
|
echo " $0 prod"
|
||||||
|
echo
|
||||||
|
echo " 方式3: PM2 ecosystem 配置"
|
||||||
|
echo " # 编辑 ecosystem.config.json 中的 env 或 env_production 节点"
|
||||||
|
echo " pm2 start ecosystem.config.json --env production"
|
||||||
|
echo
|
||||||
|
echo "必须设置的环境变量:"
|
||||||
|
echo " MONGODB_URL MongoDB连接地址"
|
||||||
|
echo " TOS_ACCESS_KEY_ID TOS访问密钥ID"
|
||||||
|
echo " TOS_ACCESS_KEY_SECRET TOS访问密钥"
|
||||||
|
echo " TOS_BUCKET_NAME TOS存储桶名称"
|
||||||
|
echo
|
||||||
|
echo "可选环境变量 (有默认值):"
|
||||||
|
echo " NODE_ENV 运行环境 (development/production)"
|
||||||
|
echo " SERVICE_ID 服务器唯一标识"
|
||||||
|
echo " SERVICE_NAME 服务器名称"
|
||||||
|
echo " HOST 绑定主机 (默认: 0.0.0.0)"
|
||||||
|
echo " PORT 端口号 (默认: 3302)"
|
||||||
|
echo " BASE_URL 服务器基础URL (自动检测)"
|
||||||
|
echo " TOS_REGION TOS地区 (默认: cn-beijing)"
|
||||||
|
echo " TOS_ENDPOINT TOS端点 (默认: tos-cn-beijing.volces.com)"
|
||||||
|
echo " TOS_SELF_DOMAIN TOS自定义域名"
|
||||||
|
echo " HEARTBEAT_ENABLED 启用心跳 (默认: true)"
|
||||||
|
echo " HEARTBEAT_INTERVAL 心跳间隔秒数 (默认: 30)"
|
||||||
|
echo
|
||||||
|
echo "示例:"
|
||||||
|
echo " # 快速开始 (使用 .env 文件)"
|
||||||
|
echo " cp .env.template .env"
|
||||||
|
echo " # 编辑 .env 文件后:"
|
||||||
|
echo " $0 prod"
|
||||||
|
echo
|
||||||
|
echo " # 临时环境变量启动"
|
||||||
|
echo " MONGODB_URL='mongodb://localhost:27017/jimeng-api' \\"
|
||||||
|
echo " TOS_ACCESS_KEY_ID='your_key' \\"
|
||||||
|
echo " TOS_ACCESS_KEY_SECRET='your_secret' \\"
|
||||||
|
echo " TOS_BUCKET_NAME='your_bucket' \\"
|
||||||
|
echo " $0 dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
local command="$1"
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
dev)
|
||||||
|
check_node_env
|
||||||
|
start_dev
|
||||||
|
;;
|
||||||
|
prod)
|
||||||
|
check_node_env
|
||||||
|
start_pm2 "production"
|
||||||
|
;;
|
||||||
|
pm2)
|
||||||
|
check_node_env
|
||||||
|
start_pm2 "development"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
check_node_env
|
||||||
|
restart_service
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
check_status
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
view_logs
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
check_node_env
|
||||||
|
build_project
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
log_error "请指定命令"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "未知命令: $command"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行主函数
|
||||||
|
main "$@"
|
||||||
@ -11,6 +11,9 @@
|
|||||||
},
|
},
|
||||||
"outDir": "./dist"
|
"outDir": "./dist"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "libs.d.ts"],
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"libs.d.ts"
|
||||||
|
],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
236
yarn.lock
236
yarn.lock
@ -56,6 +56,13 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
|
"@mongodb-js/saslprep@^1.1.9":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz"
|
||||||
|
integrity sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==
|
||||||
|
dependencies:
|
||||||
|
sparse-bitfield "^3.0.3"
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||||
@ -99,6 +106,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/http-proxy@^1.17.8":
|
||||||
|
version "1.17.16"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.16.tgz"
|
||||||
|
integrity sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/lodash@^4.14.202":
|
"@types/lodash@^4.14.202":
|
||||||
version "4.17.7"
|
version "4.17.7"
|
||||||
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz"
|
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz"
|
||||||
@ -114,6 +128,11 @@
|
|||||||
resolved "https://registry.npmmirror.com/@types/mime/-/mime-3.0.4.tgz"
|
resolved "https://registry.npmmirror.com/@types/mime/-/mime-3.0.4.tgz"
|
||||||
integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
|
integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
|
||||||
|
|
||||||
|
"@types/node-cron@^3.0.11":
|
||||||
|
version "3.0.11"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/node-cron/-/node-cron-3.0.11.tgz"
|
||||||
|
integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "20.14.12"
|
version "20.14.12"
|
||||||
resolved "https://registry.npmmirror.com/@types/node/-/node-20.14.12.tgz"
|
resolved "https://registry.npmmirror.com/@types/node/-/node-20.14.12.tgz"
|
||||||
@ -121,6 +140,33 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/webidl-conversions@*":
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz"
|
||||||
|
integrity sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==
|
||||||
|
|
||||||
|
"@types/whatwg-url@^11.0.2":
|
||||||
|
version "11.0.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-11.0.5.tgz"
|
||||||
|
integrity sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/webidl-conversions" "*"
|
||||||
|
|
||||||
|
"@volcengine/tos-sdk@^2.6.7":
|
||||||
|
version "2.7.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@volcengine/tos-sdk/-/tos-sdk-2.7.5.tgz"
|
||||||
|
integrity sha512-3qUCXegPq8GqSk4F7aHeQ0HImDttNqwnc1lGv3NRd9V0vuBTqL0450bCXeLAcMOKhfZTSuB/Bf4JbT05zLEIJA==
|
||||||
|
dependencies:
|
||||||
|
axios "^0.21.1"
|
||||||
|
axios-adapter-uniapp "^0.1.4"
|
||||||
|
crypto-js "^4.2.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
http-proxy-middleware "^2.0.1"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
qs "^6.11.2"
|
||||||
|
tos-crc64-js "0.0.1"
|
||||||
|
type-fest "^1.4.0"
|
||||||
|
|
||||||
accepts@^1.3.5:
|
accepts@^1.3.5:
|
||||||
version "1.3.8"
|
version "1.3.8"
|
||||||
resolved "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz"
|
resolved "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz"
|
||||||
@ -179,6 +225,28 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
||||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
axios-adapter-uniapp@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.npmmirror.com/axios-adapter-uniapp/-/axios-adapter-uniapp-0.1.4.tgz"
|
||||||
|
integrity sha512-4je5JcWGrrTjPEJXVXJZnOkv+BsnYn/fKbQmjxzdCGFyoQw1gq3tfQ4/WhLzy+Gi9cQJl3K8EH26G7U0BK3wcw==
|
||||||
|
dependencies:
|
||||||
|
axios "^0.27.2"
|
||||||
|
|
||||||
|
axios@^0.21.1:
|
||||||
|
version "0.21.4"
|
||||||
|
resolved "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz"
|
||||||
|
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.14.0"
|
||||||
|
|
||||||
|
axios@^0.27.2:
|
||||||
|
version "0.27.2"
|
||||||
|
resolved "https://registry.npmmirror.com/axios/-/axios-0.27.2.tgz"
|
||||||
|
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.14.9"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
|
||||||
axios@^1.6.7:
|
axios@^1.6.7:
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz"
|
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz"
|
||||||
@ -212,6 +280,11 @@ braces@^3.0.3, braces@~3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
|
bson@^6.10.4:
|
||||||
|
version "6.10.4"
|
||||||
|
resolved "https://registry.npmmirror.com/bson/-/bson-6.10.4.tgz"
|
||||||
|
integrity sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==
|
||||||
|
|
||||||
bundle-require@^5.0.0:
|
bundle-require@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.0.0.tgz"
|
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.0.0.tgz"
|
||||||
@ -370,6 +443,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3:
|
|||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
crypto-js@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz"
|
||||||
|
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||||
|
|
||||||
date-fns-tz@^3.2.0:
|
date-fns-tz@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz"
|
resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz"
|
||||||
@ -380,7 +458,7 @@ date-fns-tz@^3.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz"
|
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz"
|
||||||
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||||
|
|
||||||
debug@^4.3.2, debug@^4.3.4, debug@^4.3.5:
|
debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@4.x:
|
||||||
version "4.3.6"
|
version "4.3.6"
|
||||||
resolved "https://registry.npmmirror.com/debug/-/debug-4.3.6.tgz"
|
resolved "https://registry.npmmirror.com/debug/-/debug-4.3.6.tgz"
|
||||||
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
|
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
|
||||||
@ -513,6 +591,11 @@ escape-html@^1.0.3:
|
|||||||
resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz"
|
resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz"
|
||||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||||
|
|
||||||
|
eventemitter3@^4.0.0:
|
||||||
|
version "4.0.7"
|
||||||
|
resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz"
|
||||||
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
|
||||||
eventsource-parser@^1.1.2:
|
eventsource-parser@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz"
|
resolved "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz"
|
||||||
@ -558,7 +641,7 @@ fill-range@^7.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
follow-redirects@^1.15.6:
|
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.6:
|
||||||
version "1.15.6"
|
version "1.15.6"
|
||||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz"
|
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz"
|
||||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||||
@ -745,6 +828,26 @@ http-errors@2.0.0:
|
|||||||
statuses "2.0.1"
|
statuses "2.0.1"
|
||||||
toidentifier "1.0.1"
|
toidentifier "1.0.1"
|
||||||
|
|
||||||
|
http-proxy-middleware@^2.0.1:
|
||||||
|
version "2.0.9"
|
||||||
|
resolved "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz"
|
||||||
|
integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/http-proxy" "^1.17.8"
|
||||||
|
http-proxy "^1.18.1"
|
||||||
|
is-glob "^4.0.1"
|
||||||
|
is-plain-obj "^3.0.0"
|
||||||
|
micromatch "^4.0.2"
|
||||||
|
|
||||||
|
http-proxy@^1.18.1:
|
||||||
|
version "1.18.1"
|
||||||
|
resolved "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz"
|
||||||
|
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||||
|
dependencies:
|
||||||
|
eventemitter3 "^4.0.0"
|
||||||
|
follow-redirects "^1.0.0"
|
||||||
|
requires-port "^1.0.0"
|
||||||
|
|
||||||
human-signals@^2.1.0:
|
human-signals@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz"
|
resolved "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz"
|
||||||
@ -813,6 +916,11 @@ is-number@^7.0.0:
|
|||||||
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz"
|
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz"
|
||||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||||
|
|
||||||
|
is-plain-obj@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz"
|
||||||
|
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
|
||||||
|
|
||||||
is-stream@^2.0.0:
|
is-stream@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz"
|
resolved "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz"
|
||||||
@ -846,6 +954,11 @@ jsonfile@^6.0.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
kareem@2.6.3:
|
||||||
|
version "2.6.3"
|
||||||
|
resolved "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz"
|
||||||
|
integrity sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==
|
||||||
|
|
||||||
keygrip@~1.1.0:
|
keygrip@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz"
|
resolved "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz"
|
||||||
@ -976,6 +1089,11 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz"
|
resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz"
|
||||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||||
|
|
||||||
|
memory-pager@^1.0.2:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz"
|
||||||
|
integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
|
||||||
|
|
||||||
merge-stream@^2.0.0:
|
merge-stream@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz"
|
resolved "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz"
|
||||||
@ -991,7 +1109,7 @@ methods@^1.1.2:
|
|||||||
resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz"
|
resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz"
|
||||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||||
|
|
||||||
micromatch@^4.0.4:
|
micromatch@^4.0.2, micromatch@^4.0.4:
|
||||||
version "4.0.7"
|
version "4.0.7"
|
||||||
resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.7.tgz"
|
resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.7.tgz"
|
||||||
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
|
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
|
||||||
@ -1004,7 +1122,7 @@ mime-db@1.52.0:
|
|||||||
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
|
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
|
||||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34:
|
mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||||
version "2.1.35"
|
version "2.1.35"
|
||||||
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
|
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
@ -1038,11 +1156,58 @@ minimist@^1.2.8:
|
|||||||
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz"
|
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz"
|
||||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||||
|
|
||||||
|
mongodb-connection-string-url@^3.0.0:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz"
|
||||||
|
integrity sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==
|
||||||
|
dependencies:
|
||||||
|
"@types/whatwg-url" "^11.0.2"
|
||||||
|
whatwg-url "^14.1.0 || ^13.0.0"
|
||||||
|
|
||||||
|
mongodb@~6.18.0:
|
||||||
|
version "6.18.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mongodb/-/mongodb-6.18.0.tgz"
|
||||||
|
integrity sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==
|
||||||
|
dependencies:
|
||||||
|
"@mongodb-js/saslprep" "^1.1.9"
|
||||||
|
bson "^6.10.4"
|
||||||
|
mongodb-connection-string-url "^3.0.0"
|
||||||
|
|
||||||
|
mongoose@^8.0.3:
|
||||||
|
version "8.18.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mongoose/-/mongoose-8.18.0.tgz"
|
||||||
|
integrity sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==
|
||||||
|
dependencies:
|
||||||
|
bson "^6.10.4"
|
||||||
|
kareem "2.6.3"
|
||||||
|
mongodb "~6.18.0"
|
||||||
|
mpath "0.9.0"
|
||||||
|
mquery "5.0.0"
|
||||||
|
ms "2.1.3"
|
||||||
|
sift "17.1.3"
|
||||||
|
|
||||||
|
mpath@0.9.0:
|
||||||
|
version "0.9.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mpath/-/mpath-0.9.0.tgz"
|
||||||
|
integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==
|
||||||
|
|
||||||
|
mquery@5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mquery/-/mquery-5.0.0.tgz"
|
||||||
|
integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==
|
||||||
|
dependencies:
|
||||||
|
debug "4.x"
|
||||||
|
|
||||||
ms@2.1.2:
|
ms@2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz"
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz"
|
||||||
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
mz@^2.7.0:
|
mz@^2.7.0:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz"
|
resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz"
|
||||||
@ -1057,6 +1222,13 @@ negotiator@0.6.3:
|
|||||||
resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz"
|
resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz"
|
||||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||||
|
|
||||||
|
node-cron@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/node-cron/-/node-cron-3.0.3.tgz"
|
||||||
|
integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==
|
||||||
|
dependencies:
|
||||||
|
uuid "8.3.2"
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz"
|
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz"
|
||||||
@ -1165,12 +1337,12 @@ proxy-from-env@^1.1.0:
|
|||||||
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0, punycode@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz"
|
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
qs@^6.11.0, qs@^6.4.0, qs@^6.5.2:
|
qs@^6.11.0, qs@^6.11.2, qs@^6.4.0, qs@^6.5.2:
|
||||||
version "6.12.3"
|
version "6.12.3"
|
||||||
resolved "https://registry.npmmirror.com/qs/-/qs-6.12.3.tgz"
|
resolved "https://registry.npmmirror.com/qs/-/qs-6.12.3.tgz"
|
||||||
integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==
|
integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==
|
||||||
@ -1211,6 +1383,11 @@ readdirp@~3.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
requires-port@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz"
|
||||||
|
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
|
||||||
|
|
||||||
resolve-from@^5.0.0:
|
resolve-from@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz"
|
resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz"
|
||||||
@ -1302,6 +1479,11 @@ side-channel@^1.0.6:
|
|||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
object-inspect "^1.13.1"
|
object-inspect "^1.13.1"
|
||||||
|
|
||||||
|
sift@17.1.3:
|
||||||
|
version "17.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/sift/-/sift-17.1.3.tgz"
|
||||||
|
integrity sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==
|
||||||
|
|
||||||
signal-exit@^3.0.3:
|
signal-exit@^3.0.3:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz"
|
resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz"
|
||||||
@ -1324,6 +1506,13 @@ source-map@0.8.0-beta.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^7.0.0"
|
whatwg-url "^7.0.0"
|
||||||
|
|
||||||
|
sparse-bitfield@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz"
|
||||||
|
integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==
|
||||||
|
dependencies:
|
||||||
|
memory-pager "^1.0.2"
|
||||||
|
|
||||||
statuses@^1.5.0, "statuses@>= 1.5.0 < 2":
|
statuses@^1.5.0, "statuses@>= 1.5.0 < 2":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz"
|
resolved "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz"
|
||||||
@ -1431,6 +1620,11 @@ toidentifier@1.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz"
|
resolved "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
|
tos-crc64-js@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/tos-crc64-js/-/tos-crc64-js-0.0.1.tgz"
|
||||||
|
integrity sha512-l2Ndi9BaDH3gmAJ5VIS/1WeqFcucK41WCbw5dije24k2XtUCclYfvhntWIVHe+0S85QcED4vjqm3AO1izlQ30g==
|
||||||
|
|
||||||
tr46@^1.0.1:
|
tr46@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz"
|
resolved "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz"
|
||||||
@ -1438,6 +1632,13 @@ tr46@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
|
tr46@^5.1.0:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz"
|
||||||
|
integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
|
||||||
|
dependencies:
|
||||||
|
punycode "^2.3.1"
|
||||||
|
|
||||||
tree-kill@^1.2.2:
|
tree-kill@^1.2.2:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz"
|
resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz"
|
||||||
@ -1475,6 +1676,11 @@ tsup@^8.0.2:
|
|||||||
sucrase "^3.35.0"
|
sucrase "^3.35.0"
|
||||||
tree-kill "^1.2.2"
|
tree-kill "^1.2.2"
|
||||||
|
|
||||||
|
type-fest@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.npmmirror.com/type-fest/-/type-fest-1.4.0.tgz"
|
||||||
|
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
|
||||||
|
|
||||||
type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.18:
|
type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.18:
|
||||||
version "1.6.18"
|
version "1.6.18"
|
||||||
resolved "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz"
|
resolved "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz"
|
||||||
@ -1508,6 +1714,11 @@ uuid@^9.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz"
|
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz"
|
||||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
vary@^1.1.2:
|
vary@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz"
|
resolved "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz"
|
||||||
@ -1518,6 +1729,19 @@ webidl-conversions@^4.0.2:
|
|||||||
resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz"
|
resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz"
|
||||||
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
|
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
|
||||||
|
|
||||||
|
webidl-conversions@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz"
|
||||||
|
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
|
||||||
|
|
||||||
|
"whatwg-url@^14.1.0 || ^13.0.0":
|
||||||
|
version "14.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz"
|
||||||
|
integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
|
||||||
|
dependencies:
|
||||||
|
tr46 "^5.1.0"
|
||||||
|
webidl-conversions "^7.0.0"
|
||||||
|
|
||||||
whatwg-url@^7.0.0:
|
whatwg-url@^7.0.0:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz"
|
resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user