From 8dcdf3508f1fa801d868f0468b65977d563cbc50 Mon Sep 17 00:00:00 2001 From: jonathang4 Date: Thu, 28 Aug 2025 14:31:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=87=E6=8D=A2mongodb=E5=88=B0NeDB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.template | 4 +- configs/dev/service.yml | 6 +- package.json | 11 +- src/api/controllers/chat.ts | 4 + src/api/routes/services.ts | 164 ++++++----- src/index.ts | 29 +- src/lib/config.ts | 10 +- src/lib/configs/service-config.ts | 10 +- src/lib/database/models/GenerationResult.ts | 272 ++++++++++++++---- src/lib/database/models/GenerationTask.ts | 232 +++++++++------ src/lib/database/models/ServiceHeartbeat.ts | 54 ---- src/lib/database/mongodb.ts | 75 ----- src/lib/database/nedb.ts | 250 ++++++++++++++++ src/lib/services/DatabaseGenerationService.ts | 42 +-- src/lib/services/HeartbeatService.ts | 223 -------------- src/lib/services/NeDBCleanupService.ts | 159 ++++++++++ src/lib/services/TaskPollingService.ts | 40 +-- start-node.sh | 6 +- template.docker-compose.yml | 3 +- template.ecosystem.config.json | 18 +- yarn.lock | 212 +++++++++++--- 21 files changed, 1120 insertions(+), 704 deletions(-) delete mode 100644 src/lib/database/models/ServiceHeartbeat.ts delete mode 100644 src/lib/database/mongodb.ts create mode 100644 src/lib/database/nedb.ts delete mode 100644 src/lib/services/HeartbeatService.ts create mode 100644 src/lib/services/NeDBCleanupService.ts diff --git a/.env.template b/.env.template index bf2b1c1..d8005f1 100644 --- a/.env.template +++ b/.env.template @@ -31,10 +31,8 @@ TOS_REGION=cn-beijing TOS_ENDPOINT=tos-cn-beijing.volces.com # =========================================== -# 心跳服务配置 +# 心跳配置已移除(使用 NeDB 本地存储) # =========================================== -HEARTBEAT_ENABLED=true -HEARTBEAT_INTERVAL=30 # =========================================== # 使用方法: diff --git a/configs/dev/service.yml b/configs/dev/service.yml index 73066b7..5eb193e 100644 --- a/configs/dev/service.yml +++ b/configs/dev/service.yml @@ -3,8 +3,4 @@ name: jimeng-free-api # 服务绑定主机地址 host: '0.0.0.0' # 服务绑定端口 -port: 3302 - -heartbeat: - enabled: true - interval: 30 \ No newline at end of file +port: 3302 \ No newline at end of file diff --git a/package.json b/package.json index 848bd92..ed4c470 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "author": "Vinlic", "license": "ISC", "dependencies": { + "@seald-io/nedb": "^4.1.2", + "@volcengine/tos-sdk": "^2.6.7", "axios": "^1.6.7", "colors": "^1.4.0", "crc-32": "^1.2.2", @@ -38,14 +40,13 @@ "koa2-cors": "^2.0.6", "lodash": "^4.17.21", "mime": "^4.0.1", + "mime-types": "^2.1.35", "minimist": "^1.2.8", + + "node-cron": "^3.0.3", "randomstring": "^1.3.0", "uuid": "^9.0.1", - "yaml": "^2.3.4", - "@volcengine/tos-sdk": "^2.6.7", - "mime-types": "^2.1.35", - "mongoose": "^8.0.3", - "node-cron": "^3.0.3" + "yaml": "^2.3.4" }, "devDependencies": { "@types/lodash": "^4.14.202", diff --git a/src/api/controllers/chat.ts b/src/api/controllers/chat.ts index a866265..b984eb3 100644 --- a/src/api/controllers/chat.ts +++ b/src/api/controllers/chat.ts @@ -49,8 +49,10 @@ export async function createCompletion( const { model, width, height } = parseModel(_model); logger.info(messages); + const taskId = util.uuid(); const imageUrls = await generateImages( model, + taskId, messages[messages.length - 1].content, { width, @@ -135,8 +137,10 @@ export async function createCompletionStream( "\n\n" ); + const taskId = util.uuid(); generateImages( model, + taskId, messages[messages.length - 1].content, { width, height }, refreshToken diff --git a/src/api/routes/services.ts b/src/api/routes/services.ts index 4e11a39..6adcd52 100644 --- a/src/api/routes/services.ts +++ b/src/api/routes/services.ts @@ -1,7 +1,8 @@ -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'; +import Response from '@/lib/response/Response.js'; +import Environment from '@/lib/environment.js'; +// 心跳服务已移除 +import logger from '@/lib/logger.js'; +import nedbCleanupService from '@/lib/services/NeDBCleanupService.js'; export default { // 获取当前服务器信息 @@ -10,7 +11,14 @@ export default { // 获取所有活跃服务器 '/servers/active': async () => { 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({ success: true, @@ -29,7 +37,14 @@ export default { // 获取所有在线服务器(基于心跳超时检查) '/servers/online': async () => { 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({ success: true, @@ -48,36 +63,13 @@ export default { // 获取服务器状态统计 '/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%'; + // 心跳服务已移除,返回简单统计 + const result = { + totalServers: 1, + activeServers: 1, + onlineServers: 1, + healthRate: '100%' + }; return new Response({ success: true, @@ -96,26 +88,19 @@ export default { '/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; + // 心跳服务已移除,返回当前服务器信息 + 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 + }; return new Response({ success: true, - data: { - ...server.toObject(), - isOnline - } + data: server }); } catch (error) { logger.error('Failed to get server details:', error); @@ -124,6 +109,28 @@ export default { error: error.message }, { statusCode: 500 }); } + }, + + // 获取 NeDB 清理服务状态 + '/nedb/cleanup/status': async () => { + try { + const status = nedbCleanupService.getStatus(); + + return new Response({ + success: true, + data: { + isRunning: status.isRunning, + nextCleanup: status.nextCleanup ? new Date(status.nextCleanup).toISOString() : null, + timestamp: new Date().toISOString() + } + }); + } catch (error) { + logger.error('Failed to get NeDB cleanup status:', error); + return new Response({ + success: false, + error: error.message + }, { statusCode: 500 }); + } } }, @@ -131,7 +138,7 @@ export default { // 手动清理离线服务器 '/servers/cleanup': async () => { try { - await HeartbeatService.cleanupOfflineServers(); + // 心跳服务已移除,无需清理 return new Response({ 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项目提供) '/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 + message: 'Heartbeat service disabled (using NeDB local storage)', + data: { + server_id: serverId, + updated_at: currentTime, + is_active: true + } }); } catch (error) { logger.error('Failed to update server heartbeat:', error); diff --git a/src/index.ts b/src/index.ts index 71e9cb0..f536163 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,9 @@ import "@/lib/initialize.ts"; import server from "@/lib/server.ts"; import routes from "@/api/routes/index.ts"; import logger from "@/lib/logger.ts"; -import mongoDBManager from "@/lib/database/mongodb.ts"; -import heartbeatService from "@/lib/services/HeartbeatService.ts"; +import nedbManager from "@/lib/database/nedb.ts"; +import nedbCleanupService from "@/lib/services/NeDBCleanupService.ts"; + import taskPollingService from "@/lib/services/TaskPollingService.js"; const startupTime = performance.now(); @@ -23,30 +24,26 @@ const startupTime = performance.now(); logger.info("Service host:", config.service.host); logger.info("Service port:", config.service.port); - // 初始化MongoDB连接 + // 初始化 NeDB 数据库 try { - await mongoDBManager.connect(); - logger.success("MongoDB connected successfully"); + await nedbManager.initialize(); + logger.success("NeDB database initialized"); + + // 启动数据清理服务 + await nedbCleanupService.start(); + logger.success("NeDB cleanup service started"); } 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); 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); - } - } + // 心跳服务已移除,使用 NeDB 本地存储 // 启动任务轮询服务(仅在数据库模式下) const useDatabaseMode = process.env.USE_DATABASE_MODE === 'true'; - if (useDatabaseMode && mongoDBManager.isMongoConnected()) { + if (useDatabaseMode) { try { await taskPollingService.start(); logger.success("Task polling service started"); diff --git a/src/lib/config.ts b/src/lib/config.ts index 08a5d14..faa2834 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,5 +1,5 @@ -import serviceConfig, { ServiceConfig } from "./configs/service-config.ts"; -import systemConfig from "./configs/system-config.ts"; +import serviceConfig, { ServiceConfig } from "./configs/service-config.js"; +import systemConfig from "./configs/system-config.js"; class Config { @@ -9,11 +9,7 @@ class Config { /** 系统配置 */ system = systemConfig; - // 代理属性以便直接访问心跳和数据库配置 - get heartbeat() { - return this.service.heartbeat; - } - + // 代理属性以便直接访问数据库配置 get database() { return this.service.database; } diff --git a/src/lib/configs/service-config.ts b/src/lib/configs/service-config.ts index e2e77b1..1b82708 100644 --- a/src/lib/configs/service-config.ts +++ b/src/lib/configs/service-config.ts @@ -30,21 +30,17 @@ export class ServiceConfig { url: string; }; }; - /** 心跳配置 */ - heartbeat?: { - enabled: boolean; - interval: number; - }; + // 心跳服务已移除 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.host = _.defaultTo(host, '0.0.0.0'); this.port = _.defaultTo(port, 5566); this.urlPrefix = _.defaultTo(urlPrefix, ''); this.bindAddress = bindAddress; this.database = database; - this.heartbeat = _.defaultTo(heartbeat, { enabled: true, interval: 30 }); + // 心跳服务已移除 } get addressHost() { diff --git a/src/lib/database/models/GenerationResult.ts b/src/lib/database/models/GenerationResult.ts index 8b7ab8a..95bcd2b 100644 --- a/src/lib/database/models/GenerationResult.ts +++ b/src/lib/database/models/GenerationResult.ts @@ -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_type: 'image' | 'video'; // 任务类型 server_id: string; // 处理服务器ID @@ -31,67 +31,213 @@ export interface IGenerationResult extends Document { read_count: number; // 读取次数 } -const GenerationResultSchema: Schema = new Schema({ - task_id: { - type: String, - required: true, - unique: true - }, - task_type: { - type: String, - required: true, - enum: ['image', 'video'] - }, - server_id: { - type: String, - required: true - }, - status: { - type: String, - required: true, - enum: ['success', 'failed'] - }, - 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 +// NeDB 数据操作类 +export class GenerationResult { + private static dbName = 'generation_results'; + + // 默认过期时间:24小时(86400秒) + private static DEFAULT_EXPIRY_SECONDS = 24 * 60 * 60; + + // 创建结果 + static async create(resultData: Omit): Promise { + 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 + }; + + return await NeDBManager.insert(this.dbName, result); } -}, { - collection: 'jimeng_free_generation_results', - timestamps: false // 使用自定义时间戳 -}); -// 添加TTL索引,自动清理过期数据 -GenerationResultSchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 }); + // 查找单个结果 + static async findOne(query: any): Promise { + // 自动过滤过期数据 + const currentTime = Math.floor(Date.now() / 1000); + const filterQuery = { + ...query, + expires_at: { $gt: currentTime } + }; + + return await NeDBManager.findOne(this.dbName, filterQuery); + } -export default mongoose.model('GenerationResult', GenerationResultSchema); \ No newline at end of file + // 查找多个结果 + static async find(query: any, sort?: any, limit?: number): Promise { + // 自动过滤过期数据 + 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 { + // 自动过滤过期数据 + 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 { + // 自动过滤过期数据 + 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 { + return await NeDBManager.remove(this.dbName, query, { multi: false }); + } + + // 批量删除结果 + static async deleteMany(query: any): Promise { + return await NeDBManager.remove(this.dbName, query, { multi: true }); + } + + // 批量插入结果 + static async insertMany(results: Omit[]): Promise { + 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 { + 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 { + 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 { + const currentTime = Math.floor(Date.now() / 1000); + + return await this.deleteMany({ + expires_at: { $lte: currentTime } + }); + } + + // 获取即将过期的结果(用于预警) + static async getExpiringResults(warningSeconds: number = 3600): Promise { + 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 { + 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; \ No newline at end of file diff --git a/src/lib/database/models/GenerationTask.ts b/src/lib/database/models/GenerationTask.ts index 55a2302..bb6b4f6 100644 --- a/src/lib/database/models/GenerationTask.ts +++ b/src/lib/database/models/GenerationTask.ts @@ -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_type: 'image' | 'video'; // 任务类型 server_id: string; // 分配的服务器ID(外部分配,对应SERVICE_ID) @@ -55,85 +55,151 @@ export interface IGenerationTask extends Document { fail_code?: string; // 即梦平台返回的失败代码 } -const GenerationTaskSchema: Schema = new Schema({ - task_id: { - type: String, - 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 // 使用自定义时间戳 -}); +// NeDB 数据操作类 +export class GenerationTask { + private static dbName = 'generation_tasks'; -export default mongoose.model('GenerationTask', GenerationTaskSchema); \ No newline at end of file + // 创建任务 + static async create(taskData: Omit): Promise { + 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 { + return await NeDBManager.findOne(this.dbName, query); + } + + // 查找多个任务 + static async find(query: any, sort?: any, limit?: number): Promise { + return await NeDBManager.find(this.dbName, query, sort, limit); + } + + // 更新任务 + static async updateOne(query: any, update: any, options: any = {}): Promise { + // 自动更新 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 { + // 自动更新 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 { + return await NeDBManager.remove(this.dbName, query, { multi: false }); + } + + // 批量删除任务 + static async deleteMany(query: any): Promise { + return await NeDBManager.remove(this.dbName, query, { multi: true }); + } + + // 计数 + static async countDocuments(query: any): Promise { + return await NeDBManager.count(this.dbName, query); + } + + // 原子更新任务状态 + static async atomicUpdateTaskStatus( + taskId: string, + fromStatus: string, + toStatus: string, + additionalUpdate: any = {} + ): Promise { + 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 { + 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 { + 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 { + 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; \ No newline at end of file diff --git a/src/lib/database/models/ServiceHeartbeat.ts b/src/lib/database/models/ServiceHeartbeat.ts deleted file mode 100644 index d53e0b8..0000000 --- a/src/lib/database/models/ServiceHeartbeat.ts +++ /dev/null @@ -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('JimengServer', JimengServerSchema); \ No newline at end of file diff --git a/src/lib/database/mongodb.ts b/src/lib/database/mongodb.ts deleted file mode 100644 index 810244d..0000000 --- a/src/lib/database/mongodb.ts +++ /dev/null @@ -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 { - 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 { - 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(); \ No newline at end of file diff --git a/src/lib/database/nedb.ts b/src/lib/database/nedb.ts new file mode 100644 index 0000000..7c6767e --- /dev/null +++ b/src/lib/database/nedb.ts @@ -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 = 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 { + 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 { + 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 { + 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 { + 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 { + // NeDB 不需要显式断开连接,但可以清理资源 + this.databases.clear(); + this.isInitialized = false; + logger.info('NeDB disconnected'); + } + + // 数据库操作辅助方法 + public async insert(dbName: string, doc: any): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); \ No newline at end of file diff --git a/src/lib/services/DatabaseGenerationService.ts b/src/lib/services/DatabaseGenerationService.ts index 276c2cc..bd9b8c5 100644 --- a/src/lib/services/DatabaseGenerationService.ts +++ b/src/lib/services/DatabaseGenerationService.ts @@ -1,5 +1,7 @@ import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.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'; /** @@ -39,6 +41,9 @@ export class DatabaseGenerationService { refreshToken: string ): Promise { try { + // 确保 NeDB 数据库初始化 + await NeDBManager.initialize(); + const currentServerId = this.currentServerId; const imageTimeout = parseInt(process.env.IMAGE_TASK_TIMEOUT || '3600'); @@ -103,6 +108,9 @@ export class DatabaseGenerationService { refreshToken: string ): Promise { try { + // 确保 NeDB 数据库初始化 + await NeDBManager.initialize(); + const currentServerId = this.currentServerId; const videoTimeout = parseInt(process.env.VIDEO_TASK_TIMEOUT || '86400'); @@ -151,6 +159,9 @@ export class DatabaseGenerationService { */ async queryTaskResult(taskId: string): Promise { try { + // 确保 NeDB 数据库初始化 + await NeDBManager.initialize(); + // 1. 先查询结果表 const result = await GenerationResult.findOne({ task_id: taskId }); @@ -223,7 +234,8 @@ export class DatabaseGenerationService { // 发生错误时返回任务不存在状态 return { 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 { const filter = serverId ? { server_id: serverId } : {}; - const stats = await GenerationTask.aggregate([ - { $match: filter }, - { - $group: { - _id: "$status", - count: { $sum: 1 } - } - } - ]); - - const result = stats.reduce((acc, curr) => { - acc[curr._id] = curr.count; + // NeDB不支持aggregate,使用find替代 + const allTasks = await GenerationTask.find(filter); + const stats = allTasks.reduce((acc, task) => { + acc[task.status] = (acc[task.status] || 0) + 1; return acc; }, {} as Record); + const result = stats; + // 添加服务器负载信息 if (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) { logger.info(`Task cancelled: ${taskId}`); @@ -331,7 +337,9 @@ export class DatabaseGenerationService { fail_reason: 'Task cancelled by user' }, created_at: currentTime, - expires_at: expireTime + expires_at: expireTime, + is_read: false, + read_count: 0 }); } } @@ -364,8 +372,8 @@ export class DatabaseGenerationService { }); const cleanupStats = { - tasks: taskResult.deletedCount || 0, - results: resultResult.deletedCount || 0 + tasks: taskResult || 0, + results: resultResult || 0 }; if (cleanupStats.tasks > 0 || cleanupStats.results > 0) { diff --git a/src/lib/services/HeartbeatService.ts b/src/lib/services/HeartbeatService.ts deleted file mode 100644 index e0ef2ca..0000000 --- a/src/lib/services/HeartbeatService.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - logger.info('Received shutdown signal, stopping heartbeat service...'); - await this.stop(); - process.exit(0); - } - - // 获取所有活跃服务器 - public static async getActiveServers(): Promise { - 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 { - 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 { - 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(); \ No newline at end of file diff --git a/src/lib/services/NeDBCleanupService.ts b/src/lib/services/NeDBCleanupService.ts new file mode 100644 index 0000000..caf4cc8 --- /dev/null +++ b/src/lib/services/NeDBCleanupService.ts @@ -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 { + 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 { + 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 { + 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 { + 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(); \ No newline at end of file diff --git a/src/lib/services/TaskPollingService.ts b/src/lib/services/TaskPollingService.ts index 64203da..f690698 100644 --- a/src/lib/services/TaskPollingService.ts +++ b/src/lib/services/TaskPollingService.ts @@ -3,7 +3,7 @@ import path from 'path'; import { formatInTimeZone } from 'date-fns-tz'; import GenerationTask, { IGenerationTask } from '@/lib/database/models/GenerationTask.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 TOSService from '@/lib/tos/tos-service.js'; import { generateImages as originalGenerateImages } from '@/api/controllers/images.js'; @@ -51,7 +51,6 @@ export class TaskPollingService { /** * 启动轮询服务 - * 注意:与HeartbeatService分离,使用不同的定时器 */ public async start(): Promise { if (this.isRunning) { @@ -60,11 +59,9 @@ export class TaskPollingService { } try { - // 确保MongoDB连接可用 - if (!mongoDBManager.isMongoConnected()) { - taskLog('MongoDB not connected, skipping task polling service'); - return; - } + // 确保NeDB数据库初始化 + await NeDBManager.initialize(); + taskLog('NeDB database initialized for task polling service'); const pollIntervalMs = parseInt(process.env.TASK_POLL_INTERVAL || '5') * 1000; @@ -156,12 +153,15 @@ export class TaskPollingService { const availableSlots = this.maxConcurrentTasks - currentLoad; // 获取待处理任务 - const pendingTasks = await GenerationTask.find({ + const allTasks = await GenerationTask.find({ server_id: this.currentServerId, 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) { await this.startTask(task, currentTime); @@ -217,7 +217,7 @@ export class TaskPollingService { } ); - if (updateResult.modifiedCount === 0) { + if (updateResult === 0) { return; // 任务已被其他进程处理 } @@ -359,7 +359,9 @@ export class TaskPollingService { tos_upload_errors: tosUrls.length < originalUrls.length ? ['Some uploads failed'] : undefined }, 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 }, 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 } }); - return result.deletedCount || 0; + return result || 0; } /** @@ -909,7 +913,7 @@ export class DatabaseCleanupService { task_id: task.task_id, task_type: task.task_type, server_id: task.server_id, - status: 'failed', + status: 'failed' as const, original_urls: [], tos_urls: [], metadata: { @@ -918,7 +922,9 @@ export class DatabaseCleanupService { fail_reason: 'Task timeout after ' + task.task_timeout + ' seconds' }, 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) { diff --git a/start-node.sh b/start-node.sh index 11888df..342be79 100644 --- a/start-node.sh +++ b/start-node.sh @@ -121,8 +121,7 @@ setup_env() { 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"} + # 心跳配置已移除(使用 NeDB 本地存储) # 验证必要的环境变量 if ! validate_env; then @@ -300,8 +299,7 @@ show_help() { 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 " # 心跳配置已移除(使用 NeDB 本地存储)" echo echo "示例:" echo " # 快速开始 (使用 .env 文件)" diff --git a/template.docker-compose.yml b/template.docker-compose.yml index f8bb2b0..99951a7 100644 --- a/template.docker-compose.yml +++ b/template.docker-compose.yml @@ -23,8 +23,7 @@ services: - 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} + # 心跳配置已移除(使用 NeDB 本地存储) - USE_DATABASE_MODE=${USE_DATABASE_MODE:-false} - MAX_CONCURRENT_TASKS=${MAX_CONCURRENT_TASKS:-3} - TASK_POLL_INTERVAL=${TASK_POLL_INTERVAL:-5} diff --git a/template.ecosystem.config.json b/template.ecosystem.config.json index 67a53ae..803a496 100644 --- a/template.ecosystem.config.json +++ b/template.ecosystem.config.json @@ -20,8 +20,7 @@ "TOS_SELF_DOMAIN": "", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, @@ -42,8 +41,7 @@ "TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, @@ -80,8 +78,7 @@ "TOS_SELF_DOMAIN": "", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, @@ -102,8 +99,7 @@ "TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, @@ -140,8 +136,7 @@ "TOS_SELF_DOMAIN": "", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, @@ -162,8 +157,7 @@ "TOS_SELF_DOMAIN": "https://your-domain.com", "TOS_REGION": "cn-beijing", "TOS_ENDPOINT": "tos-cn-beijing.volces.com", - "HEARTBEAT_ENABLED": true, - "HEARTBEAT_INTERVAL": 30, + "USE_DATABASE_MODE": false, "MAX_CONCURRENT_TASKS": 3, "TASK_POLL_INTERVAL": 5, diff --git a/yarn.lock b/yarn.lock index a6bbc5a..d85bcf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -94,6 +94,20 @@ resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz" 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": version "1.0.5" 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" 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: version "0.1.4" 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" ylru "^1.2.0" -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" 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" - 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: version "3.6.0" @@ -519,6 +555,15 @@ dir-glob@^3.0.1: dependencies: 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: version "0.2.0" 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" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz" 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: version "0.23.0" 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" 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: version "3.2.1" 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" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.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: version "6.0.1" @@ -739,12 +809,10 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" @@ -758,24 +826,19 @@ has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-symbols@^1.0.3: - 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: +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" -hasown@^2.0.0: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz" 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" 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: version "2.1.0" resolved "https://registry.npmmirror.com/inflation/-/inflation-2.1.0.tgz" integrity sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ== -inherits@2.0.4: +inherits@^2.0.3, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz" 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: version "2.1.0" 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: 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: version "2.1.1" 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" 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: version "2.0.0" 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" 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: version "3.1.2" 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" 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: version "4.7.0" 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" 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: version "0.3.0" 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" 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: version "6.0.1" 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" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz" 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" 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: version "9.0.1" resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz" @@ -1751,6 +1874,19 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" 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: version "2.0.2" resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz"