即梦逆向服务 多服务部署修改 1

This commit is contained in:
jonathang4 2025-08-25 20:32:59 +08:00
parent 746d9730d8
commit 63950b18d1
32 changed files with 3928 additions and 35 deletions

47
.env.template Normal file
View 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

View File

@ -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
View 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
View File

@ -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
View 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上传失败的情况下是否正常兜底

View File

@ -4,3 +4,7 @@ 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
View 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 "$@"

View File

@ -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

View 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
View 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"
}
]
}

View 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
View 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 "$@"

View File

@ -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"
} }

View File

@ -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()) {

View File

@ -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()) {

View File

@ -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",
@ -19,10 +47,37 @@ export default {
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'){
// 任务已完成检查是否已处理TOS上传
if (!imagesTaskCache.isTosProcessed(task_id)) {
// 尚未处理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 { return {
created: util.unixTimestamp(), created: util.unixTimestamp(),
data:{task_id, url:res, status:-1}, 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(),

View File

@ -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
View 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 });
}
}
}
};

View File

@ -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",
@ -19,10 +47,37 @@ export default {
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'){
// 任务已完成检查是否已处理TOS上传
if (!videoTaskCache.isTosProcessed(task_id)) {
// 尚未处理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 { return {
created: util.unixTimestamp(), created: util.unixTimestamp(),
data:{task_id, url:res, status:-1}, 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(),

View File

@ -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);
})() })()

View File

@ -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();

View File

@ -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() {

View 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);

View 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();

View 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
View 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"
}
}
```
### 问题 4SSL 警告
**如果遇到 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
View 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
View 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
View 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
View 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 "$@"

View File

@ -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
View File

@ -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"