切换mongodb到NeDB

This commit is contained in:
jonathang4 2025-08-28 14:31:35 +08:00
parent d2c78b75d3
commit 8dcdf3508f
21 changed files with 1120 additions and 704 deletions

View File

@ -31,10 +31,8 @@ TOS_REGION=cn-beijing
TOS_ENDPOINT=tos-cn-beijing.volces.com TOS_ENDPOINT=tos-cn-beijing.volces.com
# =========================================== # ===========================================
# 心跳服务配置 # 心跳配置已移除(使用 NeDB 本地存储)
# =========================================== # ===========================================
HEARTBEAT_ENABLED=true
HEARTBEAT_INTERVAL=30
# =========================================== # ===========================================
# 使用方法: # 使用方法:

View File

@ -4,7 +4,3 @@ name: jimeng-free-api
host: '0.0.0.0' host: '0.0.0.0'
# 服务绑定端口 # 服务绑定端口
port: 3302 port: 3302
heartbeat:
enabled: true
interval: 30

View File

@ -20,6 +20,8 @@
"author": "Vinlic", "author": "Vinlic",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@seald-io/nedb": "^4.1.2",
"@volcengine/tos-sdk": "^2.6.7",
"axios": "^1.6.7", "axios": "^1.6.7",
"colors": "^1.4.0", "colors": "^1.4.0",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
@ -38,14 +40,13 @@
"koa2-cors": "^2.0.6", "koa2-cors": "^2.0.6",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.1", "mime": "^4.0.1",
"mime-types": "^2.1.35",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"node-cron": "^3.0.3",
"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",

View File

@ -49,8 +49,10 @@ export async function createCompletion(
const { model, width, height } = parseModel(_model); const { model, width, height } = parseModel(_model);
logger.info(messages); logger.info(messages);
const taskId = util.uuid();
const imageUrls = await generateImages( const imageUrls = await generateImages(
model, model,
taskId,
messages[messages.length - 1].content, messages[messages.length - 1].content,
{ {
width, width,
@ -135,8 +137,10 @@ export async function createCompletionStream(
"\n\n" "\n\n"
); );
const taskId = util.uuid();
generateImages( generateImages(
model, model,
taskId,
messages[messages.length - 1].content, messages[messages.length - 1].content,
{ width, height }, { width, height },
refreshToken refreshToken

View File

@ -1,7 +1,8 @@
import Response from '@/lib/response/Response.ts'; import Response from '@/lib/response/Response.js';
import { HeartbeatService } from '@/lib/services/HeartbeatService.ts'; import Environment from '@/lib/environment.js';
import JimengServer from '@/lib/database/models/ServiceHeartbeat.ts'; // 心跳服务已移除
import logger from '@/lib/logger.ts'; import logger from '@/lib/logger.js';
import nedbCleanupService from '@/lib/services/NeDBCleanupService.js';
export default { export default {
// 获取当前服务器信息 // 获取当前服务器信息
@ -10,7 +11,14 @@ export default {
// 获取所有活跃服务器 // 获取所有活跃服务器
'/servers/active': async () => { '/servers/active': async () => {
try { try {
const activeServers = await HeartbeatService.getActiveServers(); // 心跳服务已移除,返回当前服务器信息
const activeServers = [{
server_id: Environment.name || process.env.SERVICE_ID || 'jimeng-free-api',
server_name: Environment.name || process.env.SERVICE_NAME || 'jimeng-free-api',
host: Environment.host || process.env.SERVER_HOST || '0.0.0.0',
port: Environment.port || parseInt(process.env.SERVER_PORT || '3302'),
is_active: true
}];
return new Response({ return new Response({
success: true, success: true,
@ -29,7 +37,14 @@ export default {
// 获取所有在线服务器(基于心跳超时检查) // 获取所有在线服务器(基于心跳超时检查)
'/servers/online': async () => { '/servers/online': async () => {
try { try {
const onlineServers = await HeartbeatService.getOnlineServers(); // 心跳服务已移除,返回当前服务器信息
const onlineServers = [{
server_id: Environment.name || process.env.SERVICE_ID || 'jimeng-free-api',
server_name: Environment.name || process.env.SERVICE_NAME || 'jimeng-free-api',
host: Environment.host || process.env.SERVER_HOST || '0.0.0.0',
port: Environment.port || parseInt(process.env.SERVER_PORT || '3302'),
is_active: true
}];
return new Response({ return new Response({
success: true, success: true,
@ -48,36 +63,13 @@ export default {
// 获取服务器状态统计 // 获取服务器状态统计
'/servers/stats': async () => { '/servers/stats': async () => {
try { try {
const stats = await JimengServer.aggregate([ // 心跳服务已移除,返回简单统计
{ const result = {
$group: { totalServers: 1,
_id: null, activeServers: 1,
totalServers: { $sum: 1 }, onlineServers: 1,
activeServers: { healthRate: '100%'
$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({ return new Response({
success: true, success: true,
@ -96,29 +88,44 @@ export default {
'/servers/:serverId': async (request) => { '/servers/:serverId': async (request) => {
try { try {
const serverId = request.params.serverId; const serverId = request.params.serverId;
const server = await JimengServer.findOne({ server_id: serverId }); // 心跳服务已移除,返回当前服务器信息
const server = {
server_id: Environment.name || process.env.SERVICE_ID || 'jimeng-free-api',
server_name: Environment.name || process.env.SERVICE_NAME || 'jimeng-free-api',
host: Environment.host || process.env.SERVER_HOST || '0.0.0.0',
port: Environment.port || parseInt(process.env.SERVER_PORT || '3302'),
is_active: true,
isOnline: true
};
if (!server) { return new Response({
return new Response({ success: true,
success: false, data: server
error: 'Server not found' });
}, { statusCode: 404 }); } catch (error) {
} logger.error('Failed to get server details:', error);
return new Response({
success: false,
error: error.message
}, { statusCode: 500 });
}
},
// 检查服务器是否在线 // 获取 NeDB 清理服务状态
const currentTime = Math.floor(Date.now() / 1000); '/nedb/cleanup/status': async () => {
const heartbeatTimeout = server.heartbeat_interval * 1.5; try {
const isOnline = server.is_active && (currentTime - server.last_heartbeat) <= heartbeatTimeout; const status = nedbCleanupService.getStatus();
return new Response({ return new Response({
success: true, success: true,
data: { data: {
...server.toObject(), isRunning: status.isRunning,
isOnline nextCleanup: status.nextCleanup ? new Date(status.nextCleanup).toISOString() : null,
timestamp: new Date().toISOString()
} }
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get server details:', error); logger.error('Failed to get NeDB cleanup status:', error);
return new Response({ return new Response({
success: false, success: false,
error: error.message error: error.message
@ -131,7 +138,7 @@ export default {
// 手动清理离线服务器 // 手动清理离线服务器
'/servers/cleanup': async () => { '/servers/cleanup': async () => {
try { try {
await HeartbeatService.cleanupOfflineServers(); // 心跳服务已移除,无需清理
return new Response({ return new Response({
success: true, success: true,
@ -146,33 +153,44 @@ export default {
} }
}, },
// 手动触发 NeDB 数据清理
'/nedb/cleanup': async () => {
try {
const result = await nedbCleanupService.manualCleanup();
return new Response({
success: true,
message: 'NeDB cleanup completed successfully',
data: {
expiredResults: result.expiredResults,
expiredTasks: result.expiredTasks,
timestamp: new Date().toISOString()
}
});
} catch (error) {
logger.error('Failed to perform NeDB cleanup:', error);
return new Response({
success: false,
error: error.message
}, { statusCode: 500 });
}
},
// 更新服务器心跳为Python项目提供 // 更新服务器心跳为Python项目提供
'/servers/:serverId/heartbeat': async (request) => { '/servers/:serverId/heartbeat': async (request) => {
try { try {
const serverId = request.params.serverId; const serverId = request.params.serverId;
const currentTime = Math.floor(Date.now() / 1000); 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({ return new Response({
success: true, success: true,
message: 'Heartbeat updated successfully', message: 'Heartbeat service disabled (using NeDB local storage)',
data: result data: {
server_id: serverId,
updated_at: currentTime,
is_active: true
}
}); });
} catch (error) { } catch (error) {
logger.error('Failed to update server heartbeat:', error); logger.error('Failed to update server heartbeat:', error);

View File

@ -6,8 +6,9 @@ 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 nedbManager from "@/lib/database/nedb.ts";
import heartbeatService from "@/lib/services/HeartbeatService.ts"; import nedbCleanupService from "@/lib/services/NeDBCleanupService.ts";
import taskPollingService from "@/lib/services/TaskPollingService.js"; import taskPollingService from "@/lib/services/TaskPollingService.js";
const startupTime = performance.now(); const startupTime = performance.now();
@ -23,30 +24,26 @@ const startupTime = performance.now();
logger.info("Service host:", config.service.host); logger.info("Service host:", config.service.host);
logger.info("Service port:", config.service.port); logger.info("Service port:", config.service.port);
// 初始化MongoDB连接 // 初始化 NeDB 数据库
try { try {
await mongoDBManager.connect(); await nedbManager.initialize();
logger.success("MongoDB connected successfully"); logger.success("NeDB database initialized");
// 启动数据清理服务
await nedbCleanupService.start();
logger.success("NeDB cleanup service started");
} catch (error) { } catch (error) {
logger.warn("MongoDB connection failed, continuing without database:", error.message); logger.warn("NeDB initialization failed, continuing without database:", error.message);
} }
server.attachRoutes(routes); server.attachRoutes(routes);
await server.listen(); await server.listen();
// 启动心跳服务 // 心跳服务已移除,使用 NeDB 本地存储
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);
}
}
// 启动任务轮询服务(仅在数据库模式下) // 启动任务轮询服务(仅在数据库模式下)
const useDatabaseMode = process.env.USE_DATABASE_MODE === 'true'; const useDatabaseMode = process.env.USE_DATABASE_MODE === 'true';
if (useDatabaseMode && mongoDBManager.isMongoConnected()) { if (useDatabaseMode) {
try { try {
await taskPollingService.start(); await taskPollingService.start();
logger.success("Task polling service started"); logger.success("Task polling service started");

View File

@ -1,5 +1,5 @@
import serviceConfig, { ServiceConfig } from "./configs/service-config.ts"; import serviceConfig, { ServiceConfig } from "./configs/service-config.js";
import systemConfig from "./configs/system-config.ts"; import systemConfig from "./configs/system-config.js";
class Config { class Config {
@ -9,11 +9,7 @@ class Config {
/** 系统配置 */ /** 系统配置 */
system = systemConfig; system = systemConfig;
// 代理属性以便直接访问心跳和数据库配置 // 代理属性以便直接访问数据库配置
get heartbeat() {
return this.service.heartbeat;
}
get database() { get database() {
return this.service.database; return this.service.database;
} }

View File

@ -30,21 +30,17 @@ export class ServiceConfig {
url: string; url: string;
}; };
}; };
/** 心跳配置 */ // 心跳服务已移除
heartbeat?: {
enabled: boolean;
interval: number;
};
constructor(options?: any) { constructor(options?: any) {
const { name, host, port, urlPrefix, bindAddress, database, heartbeat } = options || {}; const { name, host, port, urlPrefix, bindAddress, database } = 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.database = database;
this.heartbeat = _.defaultTo(heartbeat, { enabled: true, interval: 30 }); // 心跳服务已移除
} }
get addressHost() { get addressHost() {

View File

@ -1,7 +1,7 @@
import mongoose, { Schema, Document } from 'mongoose'; import NeDBManager from '@/lib/database/nedb.js';
// 生成结果表数据模型 // 生成结果表数据模型
export interface IGenerationResult extends Document { export interface IGenerationResult {
task_id: string; // 关联的任务ID主键 task_id: string; // 关联的任务ID主键
task_type: 'image' | 'video'; // 任务类型 task_type: 'image' | 'video'; // 任务类型
server_id: string; // 处理服务器ID server_id: string; // 处理服务器ID
@ -31,67 +31,213 @@ export interface IGenerationResult extends Document {
read_count: number; // 读取次数 read_count: number; // 读取次数
} }
const GenerationResultSchema: Schema = new Schema({ // NeDB 数据操作类
task_id: { export class GenerationResult {
type: String, private static dbName = 'generation_results';
required: true,
unique: true // 默认过期时间24小时86400秒
}, private static DEFAULT_EXPIRY_SECONDS = 24 * 60 * 60;
task_type: {
type: String, // 创建结果
required: true, static async create(resultData: Omit<IGenerationResult, '_id'>): Promise<IGenerationResult> {
enum: ['image', 'video'] const currentTime = Math.floor(Date.now() / 1000);
}, const result = {
server_id: { ...resultData,
type: String, created_at: resultData.created_at || currentTime,
required: true expires_at: resultData.expires_at || (currentTime + this.DEFAULT_EXPIRY_SECONDS),
}, is_read: resultData.is_read || false,
status: { read_count: resultData.read_count || 0
type: String, };
required: true,
enum: ['success', 'failed'] return await NeDBManager.insert(this.dbName, result);
},
original_urls: {
type: [String],
default: []
},
tos_urls: {
type: [String],
default: []
},
metadata: {
generation_time: Number,
tos_upload_time: Number,
total_files: { type: Number, required: true },
successful_uploads: { type: Number, required: true },
tos_upload_errors: [String],
fail_reason: String
},
created_at: {
type: Number,
default: () => Math.floor(Date.now() / 1000)
},
expires_at: {
type: Number,
required: true
},
is_read: {
type: Boolean,
default: false
},
first_read_at: {
type: Number
},
read_count: {
type: Number,
default: 0
} }
}, {
collection: 'jimeng_free_generation_results',
timestamps: false // 使用自定义时间戳
});
// 添加TTL索引自动清理过期数据 // 查找单个结果
GenerationResultSchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 }); static async findOne(query: any): Promise<IGenerationResult | null> {
// 自动过滤过期数据
const currentTime = Math.floor(Date.now() / 1000);
const filterQuery = {
...query,
expires_at: { $gt: currentTime }
};
export default mongoose.model<IGenerationResult>('GenerationResult', GenerationResultSchema); return await NeDBManager.findOne(this.dbName, filterQuery);
}
// 查找多个结果
static async find(query: any, sort?: any, limit?: number): Promise<IGenerationResult[]> {
// 自动过滤过期数据
const currentTime = Math.floor(Date.now() / 1000);
const filterQuery = {
...query,
expires_at: { $gt: currentTime }
};
return await NeDBManager.find(this.dbName, filterQuery, sort, limit);
}
// 更新结果
static async updateOne(query: any, update: any, options: any = {}): Promise<number> {
// 自动过滤过期数据
const currentTime = Math.floor(Date.now() / 1000);
const filterQuery = {
...query,
expires_at: { $gt: currentTime }
};
return await NeDBManager.update(this.dbName, filterQuery, update, { ...options, multi: false });
}
// 批量更新结果
static async updateMany(query: any, update: any, options: any = {}): Promise<number> {
// 自动过滤过期数据
const currentTime = Math.floor(Date.now() / 1000);
const filterQuery = {
...query,
expires_at: { $gt: currentTime }
};
return await NeDBManager.update(this.dbName, filterQuery, update, { ...options, multi: true });
}
// 删除结果
static async deleteOne(query: any): Promise<number> {
return await NeDBManager.remove(this.dbName, query, { multi: false });
}
// 批量删除结果
static async deleteMany(query: any): Promise<number> {
return await NeDBManager.remove(this.dbName, query, { multi: true });
}
// 批量插入结果
static async insertMany(results: Omit<IGenerationResult, '_id'>[]): Promise<IGenerationResult[]> {
const insertedResults: IGenerationResult[] = [];
for (const resultData of results) {
const currentTime = Math.floor(Date.now() / 1000);
const result = {
...resultData,
created_at: resultData.created_at || currentTime,
expires_at: resultData.expires_at || (currentTime + this.DEFAULT_EXPIRY_SECONDS),
is_read: resultData.is_read || false,
read_count: resultData.read_count || 0
};
const insertedResult = await NeDBManager.insert(this.dbName, result);
insertedResults.push(insertedResult);
}
return insertedResults;
}
// 计数(排除过期数据)
static async countDocuments(query: any): Promise<number> {
const currentTime = Math.floor(Date.now() / 1000);
const filterQuery = {
...query,
expires_at: { $gt: currentTime }
};
return await NeDBManager.count(this.dbName, filterQuery);
}
// 根据任务ID查找结果并更新读取状态
static async findByTaskIdAndMarkRead(taskId: string): Promise<IGenerationResult | null> {
const result = await this.findOne({ task_id: taskId });
if (result) {
// 更新读取状态和计数
const currentTime = Math.floor(Date.now() / 1000);
await this.updateOne(
{ task_id: taskId },
{
$set: {
is_read: true,
first_read_at: result.first_read_at || currentTime
},
$inc: { read_count: 1 }
}
);
// 返回更新后的结果
result.is_read = true;
result.first_read_at = result.first_read_at || currentTime;
result.read_count = (result.read_count || 0) + 1;
}
return result;
}
// 清理过期数据
static async cleanupExpiredResults(): Promise<number> {
const currentTime = Math.floor(Date.now() / 1000);
return await this.deleteMany({
expires_at: { $lte: currentTime }
});
}
// 获取即将过期的结果(用于预警)
static async getExpiringResults(warningSeconds: number = 3600): Promise<IGenerationResult[]> {
const currentTime = Math.floor(Date.now() / 1000);
const warningTime = currentTime + warningSeconds;
return await this.find({
expires_at: {
$gt: currentTime,
$lte: warningTime
}
});
}
// 延长结果过期时间
static async extendExpiry(taskId: string, additionalSeconds: number): Promise<boolean> {
const numUpdated = await this.updateOne(
{ task_id: taskId },
{
$inc: { expires_at: additionalSeconds }
}
);
return numUpdated > 0;
}
// 获取统计信息
static async getStats(): Promise<{
total: number;
unread: number;
read: number;
byType: { [key: string]: number };
byStatus: { [key: string]: number };
}> {
const allResults = await this.find({});
const stats = {
total: allResults.length,
unread: 0,
read: 0,
byType: {} as { [key: string]: number },
byStatus: {} as { [key: string]: number }
};
allResults.forEach(result => {
// 读取状态统计
if (!result.is_read) {
stats.unread++;
} else {
stats.read++;
}
// 任务类型统计
stats.byType[result.task_type] = (stats.byType[result.task_type] || 0) + 1;
// 最终状态统计
stats.byStatus[result.status] = (stats.byStatus[result.status] || 0) + 1;
});
return stats;
}
}
export default GenerationResult;

View File

@ -1,7 +1,7 @@
import mongoose, { Schema, Document } from 'mongoose'; import NeDBManager from '@/lib/database/nedb.js';
// 生成任务表数据模型 // 生成任务表数据模型
export interface IGenerationTask extends Document { export interface IGenerationTask {
task_id: string; // 任务唯一标识,主键 task_id: string; // 任务唯一标识,主键
task_type: 'image' | 'video'; // 任务类型 task_type: 'image' | 'video'; // 任务类型
server_id: string; // 分配的服务器ID外部分配对应SERVICE_ID server_id: string; // 分配的服务器ID外部分配对应SERVICE_ID
@ -55,85 +55,151 @@ export interface IGenerationTask extends Document {
fail_code?: string; // 即梦平台返回的失败代码 fail_code?: string; // 即梦平台返回的失败代码
} }
const GenerationTaskSchema: Schema = new Schema({ // NeDB 数据操作类
task_id: { export class GenerationTask {
type: String, private static dbName = 'generation_tasks';
required: true,
unique: true
},
task_type: {
type: String,
required: true,
enum: ['image', 'video']
},
server_id: {
type: String,
required: true
},
original_params: {
model: String,
prompt: { type: String, required: true },
negative_prompt: String,
width: Number,
height: Number,
sample_strength: Number,
images: [{
url: String,
width: Number,
height: Number
}],
is_pro: Boolean,
duration: Number,
ratio: String,
response_format: String
},
internal_params: {
refresh_token: { type: String, required: true },
component_id: String,
history_id: String,
mapped_model: String,
submit_id: String
},
status: {
type: String,
required: true,
enum: ['pending', 'processing', 'polling', 'completed', 'failed'],
default: 'pending'
},
retry_count: {
type: Number,
default: 0
},
max_retries: {
type: Number,
default: 3
},
next_poll_at: {
type: Number
},
poll_interval: {
type: Number,
default: 10
},
task_timeout: {
type: Number,
required: true
},
created_at: {
type: Number,
default: () => Math.floor(Date.now() / 1000)
},
updated_at: {
type: Number,
default: () => Math.floor(Date.now() / 1000)
},
started_at: Number,
completed_at: Number,
error_message: String,
fail_code: String
}, {
collection: 'jimeng_free_generation_tasks',
timestamps: false // 使用自定义时间戳
});
export default mongoose.model<IGenerationTask>('GenerationTask', GenerationTaskSchema); // 创建任务
static async create(taskData: Omit<IGenerationTask, '_id'>): Promise<IGenerationTask> {
const currentTime = Math.floor(Date.now() / 1000);
const task = {
...taskData,
created_at: taskData.created_at || currentTime,
updated_at: taskData.updated_at || currentTime,
status: taskData.status || 'pending',
retry_count: taskData.retry_count || 0,
max_retries: taskData.max_retries || 3,
poll_interval: taskData.poll_interval || 10
};
return await NeDBManager.insert(this.dbName, task);
}
// 查找单个任务
static async findOne(query: any): Promise<IGenerationTask | null> {
return await NeDBManager.findOne(this.dbName, query);
}
// 查找多个任务
static async find(query: any, sort?: any, limit?: number): Promise<IGenerationTask[]> {
return await NeDBManager.find(this.dbName, query, sort, limit);
}
// 更新任务
static async updateOne(query: any, update: any, options: any = {}): Promise<number> {
// 自动更新 updated_at 时间戳
if (update.$set) {
update.$set.updated_at = Math.floor(Date.now() / 1000);
} else {
update.updated_at = Math.floor(Date.now() / 1000);
}
return await NeDBManager.update(this.dbName, query, update, { ...options, multi: false });
}
// 批量更新任务
static async updateMany(query: any, update: any, options: any = {}): Promise<number> {
// 自动更新 updated_at 时间戳
if (update.$set) {
update.$set.updated_at = Math.floor(Date.now() / 1000);
} else {
update.updated_at = Math.floor(Date.now() / 1000);
}
return await NeDBManager.update(this.dbName, query, update, { ...options, multi: true });
}
// 删除任务
static async deleteOne(query: any): Promise<number> {
return await NeDBManager.remove(this.dbName, query, { multi: false });
}
// 批量删除任务
static async deleteMany(query: any): Promise<number> {
return await NeDBManager.remove(this.dbName, query, { multi: true });
}
// 计数
static async countDocuments(query: any): Promise<number> {
return await NeDBManager.count(this.dbName, query);
}
// 原子更新任务状态
static async atomicUpdateTaskStatus(
taskId: string,
fromStatus: string,
toStatus: string,
additionalUpdate: any = {}
): Promise<boolean> {
const updateData = {
$set: {
status: toStatus,
updated_at: Math.floor(Date.now() / 1000),
...additionalUpdate
}
};
const numUpdated = await this.updateOne(
{ task_id: taskId, status: fromStatus },
updateData
);
return numUpdated > 0;
}
// 获取待处理的任务批次
static async getBatchPendingTasks(
serverId: string,
limit: number = 10
): Promise<IGenerationTask[]> {
const currentTime = Math.floor(Date.now() / 1000);
return await this.find(
{
server_id: serverId,
status: 'pending',
$or: [
{ next_poll_at: { $exists: false } },
{ next_poll_at: { $lte: currentTime } }
]
},
{ created_at: 1 }, // 按创建时间升序
limit
);
}
// 获取需要轮询的任务
static async getPollingTasks(
serverId: string,
limit: number = 10
): Promise<IGenerationTask[]> {
const currentTime = Math.floor(Date.now() / 1000);
return await this.find(
{
server_id: serverId,
status: 'polling',
next_poll_at: { $lte: currentTime }
},
{ next_poll_at: 1 }, // 按轮询时间升序
limit
);
}
// 获取超时的任务
static async getTimeoutTasks(): Promise<IGenerationTask[]> {
const currentTime = Math.floor(Date.now() / 1000);
return await this.find({
status: { $in: ['processing', 'polling'] },
$expr: {
$lt: [
{ $add: ['$created_at', '$task_timeout'] },
currentTime
]
}
});
}
}
export default GenerationTask;

View File

@ -1,54 +0,0 @@
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
},
server_name: {
type: String,
required: true
},
base_url: {
type: String,
required: true
},
is_active: {
type: Boolean,
default: true
},
last_heartbeat: {
type: Number,
default: 0
},
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 // 使用自定义时间戳
});
export default mongoose.model<IJimengServer>('JimengServer', JimengServerSchema);

View File

@ -1,75 +0,0 @@
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();

250
src/lib/database/nedb.ts Normal file
View File

@ -0,0 +1,250 @@
import Datastore from '@seald-io/nedb';
import logger from '@/lib/logger.js';
import path from 'path';
import fs from 'fs';
type DatastoreInstance = any;
class NeDBManager {
private static instance: NeDBManager;
private databases: Map<string, DatastoreInstance> = new Map();
private isInitialized: boolean = false;
private dataDir: string;
private constructor() {
this.dataDir = path.resolve('./data');
}
public static getInstance(): NeDBManager {
if (!NeDBManager.instance) {
NeDBManager.instance = new NeDBManager();
}
return NeDBManager.instance;
}
public async initialize(): Promise<void> {
try {
// 确保数据目录存在
// 确保目录存在
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
// 初始化数据库
await this.initDatabase('generation_tasks', {
filename: path.join(this.dataDir, 'generation_tasks.db'),
autoload: true,
timestampData: false
});
await this.initDatabase('generation_results', {
filename: path.join(this.dataDir, 'generation_results.db'),
autoload: true,
timestampData: false
});
// 创建索引
await this.createIndexes();
this.isInitialized = true;
logger.success('NeDB initialized successfully');
} catch (error) {
logger.error('NeDB initialization failed:', error);
throw error;
}
}
private async initDatabase(name: string, options: any): Promise<void> {
return new Promise((resolve, reject) => {
const db = new (Datastore as any)(options);
db.loadDatabase((err) => {
if (err) {
reject(err);
} else {
this.databases.set(name, db);
resolve();
}
});
});
}
private async createIndexes(): Promise<void> {
const tasksDb = this.getDatabase('generation_tasks');
const resultsDb = this.getDatabase('generation_results');
// 为任务表创建索引
await this.ensureIndex(tasksDb, { fieldName: 'task_id', unique: true });
await this.ensureIndex(tasksDb, { fieldName: 'status' });
await this.ensureIndex(tasksDb, { fieldName: 'server_id' });
await this.ensureIndex(tasksDb, { fieldName: 'created_at' });
await this.ensureIndex(tasksDb, { fieldName: 'next_poll_at' });
// 为结果表创建索引
await this.ensureIndex(resultsDb, { fieldName: 'task_id', unique: true });
await this.ensureIndex(resultsDb, { fieldName: 'expires_at' });
await this.ensureIndex(resultsDb, { fieldName: 'created_at' });
logger.info('NeDB indexes created successfully');
}
private async ensureIndex(db: DatastoreInstance, options: any): Promise<void> {
return new Promise((resolve, reject) => {
db.ensureIndex(options, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
public getDatabase(name: string): DatastoreInstance {
const db = this.databases.get(name);
if (!db) {
throw new Error(`Database ${name} not found`);
}
return db;
}
public isNeDBConnected(): boolean {
return this.isInitialized;
}
public async disconnect(): Promise<void> {
// NeDB 不需要显式断开连接,但可以清理资源
this.databases.clear();
this.isInitialized = false;
logger.info('NeDB disconnected');
}
// 数据库操作辅助方法
public async insert(dbName: string, doc: any): Promise<any> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
db.insert(doc, (err, newDoc) => {
if (err) {
reject(err);
} else {
resolve(newDoc);
}
});
});
}
public async findOne(dbName: string, query: any): Promise<any> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
db.findOne(query, (err, doc) => {
if (err) {
reject(err);
} else {
resolve(doc);
}
});
});
}
public async find(dbName: string, query: any, sort?: any, limit?: number): Promise<any[]> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
let cursor = db.find(query);
if (sort) {
cursor = cursor.sort(sort);
}
if (limit) {
cursor = cursor.limit(limit);
}
cursor.exec((err, docs) => {
if (err) {
reject(err);
} else {
resolve(docs);
}
});
});
}
public async update(dbName: string, query: any, update: any, options: any = {}): Promise<number> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
db.update(query, update, options, (err, numReplaced) => {
if (err) {
reject(err);
} else {
resolve(numReplaced);
}
});
});
}
public async remove(dbName: string, query: any, options: any = {}): Promise<number> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
db.remove(query, options, (err, numRemoved) => {
if (err) {
reject(err);
} else {
resolve(numRemoved);
}
});
});
}
public async count(dbName: string, query: any): Promise<number> {
const db = this.getDatabase(dbName);
return new Promise((resolve, reject) => {
db.count(query, (err, count) => {
if (err) {
reject(err);
} else {
resolve(count);
}
});
});
}
// 数据过期清理方法
public async cleanupExpiredData(): Promise<void> {
const currentTime = Math.floor(Date.now() / 1000);
try {
// 清理过期的结果数据
const removedResults = await this.remove('generation_results', {
expires_at: { $lt: currentTime }
}, { multi: true });
// 清理超过24小时的已完成任务
const taskExpireTime = currentTime - (24 * 60 * 60); // 24小时前
const removedTasks = await this.remove('generation_tasks', {
$and: [
{ status: { $in: ['completed', 'failed'] } },
{ updated_at: { $lt: taskExpireTime } }
]
}, { multi: true });
if (removedResults > 0 || removedTasks > 0) {
logger.info(`Cleaned up expired data: ${removedResults} results, ${removedTasks} tasks`);
}
} catch (error) {
logger.error('Failed to cleanup expired data:', error);
}
}
// 启动定期清理
public startPeriodicCleanup(): void {
// 每小时清理一次过期数据
setInterval(() => {
this.cleanupExpiredData();
}, 60 * 60 * 1000); // 1小时
logger.info('Periodic cleanup started for NeDB');
}
}
export default NeDBManager.getInstance();

View File

@ -1,5 +1,7 @@
import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.js'; import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.js';
import GenerationResult, { IGenerationResult } from '@/lib/database/models/GenerationResult.js'; import GenerationResult, { IGenerationResult } from '@/lib/database/models/GenerationResult.js';
import NeDBManager from '@/lib/database/nedb.js';
import Environment from '@/lib/environment.js';
import logger from '@/lib/logger.js'; import logger from '@/lib/logger.js';
/** /**
@ -39,6 +41,9 @@ export class DatabaseGenerationService {
refreshToken: string refreshToken: string
): Promise<void> { ): Promise<void> {
try { try {
// 确保 NeDB 数据库初始化
await NeDBManager.initialize();
const currentServerId = this.currentServerId; const currentServerId = this.currentServerId;
const imageTimeout = parseInt(process.env.IMAGE_TASK_TIMEOUT || '3600'); const imageTimeout = parseInt(process.env.IMAGE_TASK_TIMEOUT || '3600');
@ -103,6 +108,9 @@ export class DatabaseGenerationService {
refreshToken: string refreshToken: string
): Promise<void> { ): Promise<void> {
try { try {
// 确保 NeDB 数据库初始化
await NeDBManager.initialize();
const currentServerId = this.currentServerId; const currentServerId = this.currentServerId;
const videoTimeout = parseInt(process.env.VIDEO_TASK_TIMEOUT || '86400'); const videoTimeout = parseInt(process.env.VIDEO_TASK_TIMEOUT || '86400');
@ -151,6 +159,9 @@ export class DatabaseGenerationService {
*/ */
async queryTaskResult(taskId: string): Promise<any> { async queryTaskResult(taskId: string): Promise<any> {
try { try {
// 确保 NeDB 数据库初始化
await NeDBManager.initialize();
// 1. 先查询结果表 // 1. 先查询结果表
const result = await GenerationResult.findOne({ task_id: taskId }); const result = await GenerationResult.findOne({ task_id: taskId });
@ -223,7 +234,8 @@ export class DatabaseGenerationService {
// 发生错误时返回任务不存在状态 // 发生错误时返回任务不存在状态
return { return {
created: Math.floor(Date.now() / 1000), created: Math.floor(Date.now() / 1000),
data: { task_id: taskId, url: "", status: 0 } data: { task_id: taskId, url: "", status: 0 },
message: `接口报错 taskId: ${taskId} error:${error.message}`,
}; };
} }
} }
@ -235,21 +247,15 @@ export class DatabaseGenerationService {
try { try {
const filter = serverId ? { server_id: serverId } : {}; const filter = serverId ? { server_id: serverId } : {};
const stats = await GenerationTask.aggregate([ // NeDB不支持aggregate使用find替代
{ $match: filter }, const allTasks = await GenerationTask.find(filter);
{ const stats = allTasks.reduce((acc, task) => {
$group: { acc[task.status] = (acc[task.status] || 0) + 1;
_id: "$status",
count: { $sum: 1 }
}
}
]);
const result = stats.reduce((acc, curr) => {
acc[curr._id] = curr.count;
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number>);
const result = stats;
// 添加服务器负载信息 // 添加服务器负载信息
if (serverId) { if (serverId) {
result['server_load'] = await this.getServerLoad(serverId); result['server_load'] = await this.getServerLoad(serverId);
@ -307,7 +313,7 @@ export class DatabaseGenerationService {
} }
); );
const cancelled = result.modifiedCount > 0; const cancelled = result > 0;
if (cancelled) { if (cancelled) {
logger.info(`Task cancelled: ${taskId}`); logger.info(`Task cancelled: ${taskId}`);
@ -331,7 +337,9 @@ export class DatabaseGenerationService {
fail_reason: 'Task cancelled by user' fail_reason: 'Task cancelled by user'
}, },
created_at: currentTime, created_at: currentTime,
expires_at: expireTime expires_at: expireTime,
is_read: false,
read_count: 0
}); });
} }
} }
@ -364,8 +372,8 @@ export class DatabaseGenerationService {
}); });
const cleanupStats = { const cleanupStats = {
tasks: taskResult.deletedCount || 0, tasks: taskResult || 0,
results: resultResult.deletedCount || 0 results: resultResult || 0
}; };
if (cleanupStats.tasks > 0 || cleanupStats.results > 0) { if (cleanupStats.tasks > 0 || cleanupStats.results > 0) {

View File

@ -1,223 +0,0 @@
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 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;
}
public static getInstance(): HeartbeatService {
if (!HeartbeatService.instance) {
HeartbeatService.instance = new HeartbeatService();
}
return HeartbeatService.instance;
}
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 existingServer = await JimengServer.findOne({ server_id: this.serverId });
if (!existingServer) {
logger.error(`服务器信息不存在 serverId: ${this.serverId}`);
// } else {
// // 服务器已存在:只更新心跳相关字段,不修改 base_url
// await JimengServer.findOneAndUpdate(
// { server_id: this.serverId },
// {
// server_name: this.serverName, // 允许更新服务器名称
// is_active: true,
// last_heartbeat: currentTime,
// heartbeat_interval: parseInt(process.env.HEARTBEAT_INTERVAL || '60'),
// updated_at: currentTime
// // 注意:不更新 base_url这由其他服务维护
// }
// );
// logger.info(`Server re-registered: ${this.serverId} (base_url preserved)`);
}
} catch (error) {
logger.error('Failed to register server:', error);
throw error;
}
}
private async sendHeartbeat(): Promise<void> {
try {
const currentTime = Math.floor(Date.now() / 1000);
// 只更新心跳相关字段,不修改 base_url 等其他服务信息
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 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();

View File

@ -0,0 +1,159 @@
import logger from '@/lib/logger.js';
import { GenerationTask } from '@/lib/database/models/GenerationTask.js';
import { GenerationResult } from '@/lib/database/models/GenerationResult.js';
/**
* NeDB
*
*/
export class NeDBCleanupService {
private static instance: NeDBCleanupService;
private cleanupInterval: NodeJS.Timeout | null = null;
private readonly CLEANUP_INTERVAL = 60 * 60 * 1000; // 1小时清理一次
private isRunning = false;
private constructor() {}
static getInstance(): NeDBCleanupService {
if (!NeDBCleanupService.instance) {
NeDBCleanupService.instance = new NeDBCleanupService();
}
return NeDBCleanupService.instance;
}
/**
*
*/
async start(): Promise<void> {
if (this.isRunning) {
logger.warn('NeDB cleanup service is already running');
return;
}
this.isRunning = true;
logger.info('Starting NeDB cleanup service...');
// 立即执行一次清理
await this.performCleanup();
// 设置定期清理
this.cleanupInterval = setInterval(async () => {
await this.performCleanup();
}, this.CLEANUP_INTERVAL);
logger.success('NeDB cleanup service started successfully');
}
/**
*
*/
stop(): void {
if (!this.isRunning) {
logger.warn('NeDB cleanup service is not running');
return;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.isRunning = false;
logger.info('NeDB cleanup service stopped');
}
/**
*
*/
private async performCleanup(): Promise<void> {
try {
logger.info('Starting NeDB data cleanup...');
const startTime = Date.now();
// 清理过期的结果数据
const expiredResultsCount = await this.cleanupExpiredResults();
// 清理过期的任务数据
const expiredTasksCount = await this.cleanupExpiredTasks();
const duration = Date.now() - startTime;
logger.success(
`NeDB cleanup completed in ${duration}ms. ` +
`Cleaned ${expiredResultsCount} expired results and ${expiredTasksCount} expired tasks`
);
} catch (error) {
logger.error('Failed to perform NeDB cleanup:', error);
}
}
/**
*
*/
private async cleanupExpiredResults(): Promise<number> {
try {
const deletedCount = await GenerationResult.cleanupExpiredResults();
if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} expired generation results`);
}
return deletedCount;
} catch (error) {
logger.error('Failed to cleanup expired results:', error);
return 0;
}
}
/**
*
* 7
*/
private async cleanupExpiredTasks(): Promise<number> {
try {
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
// 删除超过 7 天的已完成任务
const deletedCount = await GenerationTask.deleteMany({
status: { $in: ['completed', 'failed', 'cancelled'] },
created_at: { $lt: sevenDaysAgo }
});
if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} expired generation tasks`);
}
return deletedCount;
} catch (error) {
logger.error('Failed to cleanup expired tasks:', error);
return 0;
}
}
/**
*
*/
async manualCleanup(): Promise<{ expiredResults: number; expiredTasks: number }> {
logger.info('Manual NeDB cleanup triggered');
const expiredResults = await this.cleanupExpiredResults();
const expiredTasks = await this.cleanupExpiredTasks();
return { expiredResults, expiredTasks };
}
/**
*
*/
getStatus(): { isRunning: boolean; nextCleanup?: number } {
const status: { isRunning: boolean; nextCleanup?: number } = {
isRunning: this.isRunning
};
if (this.isRunning && this.cleanupInterval) {
// 估算下次清理时间(不完全准确,但提供参考)
status.nextCleanup = Date.now() + this.CLEANUP_INTERVAL;
}
return status;
}
}
// 导出单例实例
export default NeDBCleanupService.getInstance();

View File

@ -3,7 +3,7 @@ import path from 'path';
import { formatInTimeZone } from 'date-fns-tz'; import { formatInTimeZone } from 'date-fns-tz';
import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.js'; import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.js';
import GenerationResult, { IGenerationResult } from '@/lib/database/models/GenerationResult.js'; import GenerationResult, { IGenerationResult } from '@/lib/database/models/GenerationResult.js';
import mongoDBManager from '@/lib/database/mongodb.js'; import NeDBManager from '@/lib/database/nedb.js';
import logger from '@/lib/logger.js'; import logger from '@/lib/logger.js';
import TOSService from '@/lib/tos/tos-service.js'; import TOSService from '@/lib/tos/tos-service.js';
import { generateImages as originalGenerateImages } from '@/api/controllers/images.js'; import { generateImages as originalGenerateImages } from '@/api/controllers/images.js';
@ -51,7 +51,6 @@ export class TaskPollingService {
/** /**
* *
* HeartbeatService分离使
*/ */
public async start(): Promise<void> { public async start(): Promise<void> {
if (this.isRunning) { if (this.isRunning) {
@ -60,11 +59,9 @@ export class TaskPollingService {
} }
try { try {
// 确保MongoDB连接可用 // 确保NeDB数据库初始化
if (!mongoDBManager.isMongoConnected()) { await NeDBManager.initialize();
taskLog('MongoDB not connected, skipping task polling service'); taskLog('NeDB database initialized for task polling service');
return;
}
const pollIntervalMs = parseInt(process.env.TASK_POLL_INTERVAL || '5') * 1000; const pollIntervalMs = parseInt(process.env.TASK_POLL_INTERVAL || '5') * 1000;
@ -156,12 +153,15 @@ export class TaskPollingService {
const availableSlots = this.maxConcurrentTasks - currentLoad; const availableSlots = this.maxConcurrentTasks - currentLoad;
// 获取待处理任务 // 获取待处理任务
const pendingTasks = await GenerationTask.find({ const allTasks = await GenerationTask.find({
server_id: this.currentServerId, server_id: this.currentServerId,
status: 'pending' status: 'pending'
}) });
.sort({ created_at: 1 }) // 先入先出
.limit(availableSlots); // 手动排序并限制数量
const pendingTasks = allTasks
.sort((a, b) => a.created_at - b.created_at) // 先入先出
.slice(0, availableSlots);
for (const task of pendingTasks) { for (const task of pendingTasks) {
await this.startTask(task, currentTime); await this.startTask(task, currentTime);
@ -217,7 +217,7 @@ export class TaskPollingService {
} }
); );
if (updateResult.modifiedCount === 0) { if (updateResult === 0) {
return; // 任务已被其他进程处理 return; // 任务已被其他进程处理
} }
@ -359,7 +359,9 @@ export class TaskPollingService {
tos_upload_errors: tosUrls.length < originalUrls.length ? ['Some uploads failed'] : undefined tos_upload_errors: tosUrls.length < originalUrls.length ? ['Some uploads failed'] : undefined
}, },
created_at: currentTime, created_at: currentTime,
expires_at: expireTime expires_at: expireTime,
is_read: false,
read_count: 0
}); });
// 标记任务完成 // 标记任务完成
@ -403,7 +405,9 @@ export class TaskPollingService {
fail_reason: errorMessage fail_reason: errorMessage
}, },
created_at: now, created_at: now,
expires_at: expireTime expires_at: expireTime,
is_read: false,
read_count: 0
}); });
// 标记任务失败 // 标记任务失败
@ -870,7 +874,7 @@ export class DatabaseCleanupService {
expires_at: { $lt: currentTime } expires_at: { $lt: currentTime }
}); });
return result.deletedCount || 0; return result || 0;
} }
/** /**
@ -909,7 +913,7 @@ export class DatabaseCleanupService {
task_id: task.task_id, task_id: task.task_id,
task_type: task.task_type, task_type: task.task_type,
server_id: task.server_id, server_id: task.server_id,
status: 'failed', status: 'failed' as const,
original_urls: [], original_urls: [],
tos_urls: [], tos_urls: [],
metadata: { metadata: {
@ -918,7 +922,9 @@ export class DatabaseCleanupService {
fail_reason: 'Task timeout after ' + task.task_timeout + ' seconds' fail_reason: 'Task timeout after ' + task.task_timeout + ' seconds'
}, },
created_at: currentTime, created_at: currentTime,
expires_at: currentTime + parseInt(process.env.RESULT_EXPIRE_TIME || '86400') expires_at: currentTime + parseInt(process.env.RESULT_EXPIRE_TIME || '86400'),
is_read: false,
read_count: 0
})); }));
if (failedResults.length > 0) { if (failedResults.length > 0) {

View File

@ -121,8 +121,7 @@ setup_env() {
export PORT=${PORT:-"3302"} export PORT=${PORT:-"3302"}
export TOS_REGION=${TOS_REGION:-"cn-beijing"} export TOS_REGION=${TOS_REGION:-"cn-beijing"}
export TOS_ENDPOINT=${TOS_ENDPOINT:-"tos-cn-beijing.volces.com"} export TOS_ENDPOINT=${TOS_ENDPOINT:-"tos-cn-beijing.volces.com"}
export HEARTBEAT_ENABLED=${HEARTBEAT_ENABLED:-"true"} # 心跳配置已移除(使用 NeDB 本地存储)
export HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-"30"}
# 验证必要的环境变量 # 验证必要的环境变量
if ! validate_env; then if ! validate_env; then
@ -300,8 +299,7 @@ show_help() {
echo " TOS_REGION TOS地区 (默认: cn-beijing)" echo " TOS_REGION TOS地区 (默认: cn-beijing)"
echo " TOS_ENDPOINT TOS端点 (默认: tos-cn-beijing.volces.com)" echo " TOS_ENDPOINT TOS端点 (默认: tos-cn-beijing.volces.com)"
echo " TOS_SELF_DOMAIN TOS自定义域名" echo " TOS_SELF_DOMAIN TOS自定义域名"
echo " HEARTBEAT_ENABLED 启用心跳 (默认: true)" echo " # 心跳配置已移除(使用 NeDB 本地存储)"
echo " HEARTBEAT_INTERVAL 心跳间隔秒数 (默认: 30)"
echo echo
echo "示例:" echo "示例:"
echo " # 快速开始 (使用 .env 文件)" echo " # 快速开始 (使用 .env 文件)"

View File

@ -23,8 +23,7 @@ services:
- TOS_SELF_DOMAIN=${TOS_SELF_DOMAIN} - TOS_SELF_DOMAIN=${TOS_SELF_DOMAIN}
- TOS_REGION=${TOS_REGION:-cn-beijing} - TOS_REGION=${TOS_REGION:-cn-beijing}
- TOS_ENDPOINT=${TOS_ENDPOINT:-tos-cn-beijing.volces.com} - TOS_ENDPOINT=${TOS_ENDPOINT:-tos-cn-beijing.volces.com}
- HEARTBEAT_ENABLED=${HEARTBEAT_ENABLED:-true} # 心跳配置已移除(使用 NeDB 本地存储)
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
- USE_DATABASE_MODE=${USE_DATABASE_MODE:-false} - USE_DATABASE_MODE=${USE_DATABASE_MODE:-false}
- MAX_CONCURRENT_TASKS=${MAX_CONCURRENT_TASKS:-3} - MAX_CONCURRENT_TASKS=${MAX_CONCURRENT_TASKS:-3}
- TASK_POLL_INTERVAL=${TASK_POLL_INTERVAL:-5} - TASK_POLL_INTERVAL=${TASK_POLL_INTERVAL:-5}

View File

@ -20,8 +20,7 @@
"TOS_SELF_DOMAIN": "", "TOS_SELF_DOMAIN": "",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,
@ -42,8 +41,7 @@
"TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_SELF_DOMAIN": "https://your-domain.com",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,
@ -80,8 +78,7 @@
"TOS_SELF_DOMAIN": "", "TOS_SELF_DOMAIN": "",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,
@ -102,8 +99,7 @@
"TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_SELF_DOMAIN": "https://your-domain.com",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,
@ -140,8 +136,7 @@
"TOS_SELF_DOMAIN": "", "TOS_SELF_DOMAIN": "",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,
@ -162,8 +157,7 @@
"TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_SELF_DOMAIN": "https://your-domain.com",
"TOS_REGION": "cn-beijing", "TOS_REGION": "cn-beijing",
"TOS_ENDPOINT": "tos-cn-beijing.volces.com", "TOS_ENDPOINT": "tos-cn-beijing.volces.com",
"HEARTBEAT_ENABLED": true,
"HEARTBEAT_INTERVAL": 30,
"USE_DATABASE_MODE": false, "USE_DATABASE_MODE": false,
"MAX_CONCURRENT_TASKS": 3, "MAX_CONCURRENT_TASKS": 3,
"TASK_POLL_INTERVAL": 5, "TASK_POLL_INTERVAL": 5,

212
yarn.lock
View File

@ -94,6 +94,20 @@
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz" resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz"
integrity sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg== integrity sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==
"@seald-io/binary-search-tree@^1.0.3":
version "1.0.3"
resolved "https://registry.npmmirror.com/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz"
integrity sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==
"@seald-io/nedb@^4.1.2":
version "4.1.2"
resolved "https://registry.npmmirror.com/@seald-io/nedb/-/nedb-4.1.2.tgz"
integrity sha512-bDr6TqjBVS2rDyYM9CPxAnotj5FuNL9NF8o7h7YyFXM7yruqT4ddr+PkSb2mJvvw991bqdftazkEo38gykvaww==
dependencies:
"@seald-io/binary-search-tree" "^1.0.3"
localforage "^1.10.0"
util "^0.12.5"
"@types/estree@1.0.5": "@types/estree@1.0.5":
version "1.0.5" version "1.0.5"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz" resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz"
@ -225,6 +239,13 @@ 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==
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
dependencies:
possible-typed-array-names "^1.0.0"
axios-adapter-uniapp@^0.1.4: axios-adapter-uniapp@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/axios-adapter-uniapp/-/axios-adapter-uniapp-0.1.4.tgz" resolved "https://registry.npmmirror.com/axios-adapter-uniapp/-/axios-adapter-uniapp-0.1.4.tgz"
@ -310,16 +331,31 @@ cache-content-type@^1.0.0:
mime-types "^2.1.18" mime-types "^2.1.18"
ylru "^1.2.0" ylru "^1.2.0"
call-bind@^1.0.7: call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.7" version "1.0.2"
resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz" resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies: dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0" es-errors "^1.3.0"
function-bind "^1.1.2" function-bind "^1.1.2"
call-bind@^1.0.7, call-bind@^1.0.8:
version "1.0.8"
resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz"
integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
dependencies:
call-bind-apply-helpers "^1.0.0"
es-define-property "^1.0.0"
get-intrinsic "^1.2.4" get-intrinsic "^1.2.4"
set-function-length "^1.2.1" set-function-length "^1.2.2"
call-bound@^1.0.2, call-bound@^1.0.4:
version "1.0.4"
resolved "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz"
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
chokidar@^3.6.0: chokidar@^3.6.0:
version "3.6.0" version "3.6.0"
@ -519,6 +555,15 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
eastasianwidth@^0.2.0: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz" resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
@ -544,18 +589,23 @@ encodeurl@^1.0.2:
resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz" resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
es-define-property@^1.0.0: es-define-property@^1.0.0, es-define-property@^1.0.1:
version "1.0.0" version "1.0.1"
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz" resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0: es-errors@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz" resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
esbuild@^0.23.0, esbuild@>=0.18: esbuild@^0.23.0, esbuild@>=0.18:
version "0.23.0" version "0.23.0"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.23.0.tgz" resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.23.0.tgz"
@ -646,6 +696,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9, fol
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==
for-each@^0.3.5:
version "0.3.5"
resolved "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz"
integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
dependencies:
is-callable "^1.2.7"
foreground-child@^3.1.0: foreground-child@^3.1.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.2.1.tgz" resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.2.1.tgz"
@ -692,16 +749,29 @@ function-bind@^1.1.2:
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: get-intrinsic@^1.2.4, get-intrinsic@^1.3.0:
version "1.2.4" version "1.3.0"
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz" resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies: dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0" es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2" function-bind "^1.1.2"
has-proto "^1.0.1" get-proto "^1.0.1"
has-symbols "^1.0.3" gopd "^1.2.0"
hasown "^2.0.0" has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-stream@^6.0.0: get-stream@^6.0.0:
version "6.0.1" version "6.0.1"
@ -739,12 +809,10 @@ globby@^11.1.0:
merge2 "^1.4.1" merge2 "^1.4.1"
slash "^3.0.0" slash "^3.0.0"
gopd@^1.0.1: gopd@^1.0.1, gopd@^1.2.0:
version "1.0.1" version "1.2.0"
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz" resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz"
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
dependencies:
get-intrinsic "^1.1.3"
graceful-fs@^4.1.6, graceful-fs@^4.2.0: graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.11" version "4.2.11"
@ -758,24 +826,19 @@ has-property-descriptors@^1.0.2:
dependencies: dependencies:
es-define-property "^1.0.0" es-define-property "^1.0.0"
has-proto@^1.0.1: has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.0.3" version "1.1.0"
resolved "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz" resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz"
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-symbols@^1.0.3: has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
version "1.0.3"
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has-tostringtag@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz" resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies: dependencies:
has-symbols "^1.0.3" has-symbols "^1.0.3"
hasown@^2.0.0: hasown@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz" resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
@ -870,16 +933,29 @@ image-size@^2.0.2:
resolved "https://registry.npmmirror.com/image-size/-/image-size-2.0.2.tgz" resolved "https://registry.npmmirror.com/image-size/-/image-size-2.0.2.tgz"
integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
inflation@^2.0.0: inflation@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/inflation/-/inflation-2.1.0.tgz" resolved "https://registry.npmmirror.com/inflation/-/inflation-2.1.0.tgz"
integrity sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ== integrity sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==
inherits@2.0.4: inherits@^2.0.3, inherits@2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz" resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
is-arguments@^1.0.4:
version "1.2.0"
resolved "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz"
integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==
dependencies:
call-bound "^1.0.2"
has-tostringtag "^1.0.2"
is-binary-path@~2.1.0: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz" resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz"
@ -887,6 +963,11 @@ is-binary-path@~2.1.0:
dependencies: dependencies:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-callable@^1.2.7:
version "1.2.7"
resolved "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-extglob@^2.1.1: is-extglob@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz" resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz"
@ -926,6 +1007,13 @@ is-stream@^2.0.0:
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"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-typed-array@^1.1.3:
version "1.1.15"
resolved "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz"
integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
dependencies:
which-typed-array "^1.1.16"
isexe@^2.0.0: isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz" resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz"
@ -1049,6 +1137,13 @@ koa2-cors@^2.0.6:
resolved "https://registry.npmmirror.com/koa2-cors/-/koa2-cors-2.0.6.tgz" resolved "https://registry.npmmirror.com/koa2-cors/-/koa2-cors-2.0.6.tgz"
integrity sha512-JRCcSM4lamM+8kvKGDKlesYk2ASrmSTczDtGUnIadqMgnHU4Ct5Gw7Bxt3w3m6d6dy3WN0PU4oMP43HbddDEWg== integrity sha512-JRCcSM4lamM+8kvKGDKlesYk2ASrmSTczDtGUnIadqMgnHU4Ct5Gw7Bxt3w3m6d6dy3WN0PU4oMP43HbddDEWg==
lie@3.1.1:
version "3.1.1"
resolved "https://registry.npmmirror.com/lie/-/lie-3.1.1.tgz"
integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
dependencies:
immediate "~3.0.5"
lilconfig@^3.1.1: lilconfig@^3.1.1:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.2.tgz" resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.2.tgz"
@ -1064,6 +1159,13 @@ load-tsconfig@^0.2.3:
resolved "https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz" resolved "https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz"
integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==
localforage@^1.10.0:
version "1.10.0"
resolved "https://registry.npmmirror.com/localforage/-/localforage-1.10.0.tgz"
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
dependencies:
lie "3.1.1"
lodash.sortby@^4.7.0: lodash.sortby@^4.7.0:
version "4.7.0" version "4.7.0"
resolved "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz" resolved "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz"
@ -1084,6 +1186,11 @@ luxon@~3.4.0:
resolved "https://registry.npmmirror.com/luxon/-/luxon-3.4.4.tgz" resolved "https://registry.npmmirror.com/luxon/-/luxon-3.4.4.tgz"
integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "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"
@ -1325,6 +1432,11 @@ pirates@^4.0.1:
resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz" resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss-load-config@^6.0.1: postcss-load-config@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz" resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz"
@ -1440,7 +1552,7 @@ safe-buffer@5.2.1:
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz" resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
set-function-length@^1.2.1: set-function-length@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz" resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
@ -1709,6 +1821,17 @@ unpipe@1.0.0:
resolved "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz" resolved "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
util@^0.12.5:
version "0.12.5"
resolved "https://registry.npmmirror.com/util/-/util-0.12.5.tgz"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
which-typed-array "^1.1.2"
uuid@^9.0.1: uuid@^9.0.1:
version "9.0.1" version "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"
@ -1751,6 +1874,19 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1" tr46 "^1.0.1"
webidl-conversions "^4.0.2" webidl-conversions "^4.0.2"
which-typed-array@^1.1.16, which-typed-array@^1.1.2:
version "1.1.19"
resolved "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz"
integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
dependencies:
available-typed-arrays "^1.0.7"
call-bind "^1.0.8"
call-bound "^1.0.4"
for-each "^0.3.5"
get-proto "^1.0.1"
gopd "^1.2.0"
has-tostringtag "^1.0.2"
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz" resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz"