Download scripts/knowledgeContentScheduler.js from rxb0506/lifekline: direct link, hf CLI and curl.
- Browser
- Download file 7.82 kB
-
https://huggingface.co/spaces/rxb0506/lifekline/resolve/main/scripts/knowledgeContentScheduler.js
- Command line
-
hf download hf://spaces/rxb0506/lifekline/scripts/knowledgeContentScheduler.js
-
curl -L -o knowledgeContentScheduler.js https://huggingface.co/spaces/rxb0506/lifekline/resolve/main/scripts/knowledgeContentScheduler.js
7.82 kB
| /** | |
| * 知识内容调度器 (长期运行) | |
| * | |
| * 功能: | |
| * 1. 定时触发内容生成 | |
| * 2. 按阶段自动调整生成频率 | |
| * 3. 自动重试失败任务 | |
| * 4. 日志记录与状态报告 | |
| * | |
| * 使用方法: | |
| * nohup node scripts/knowledgeContentScheduler.js > logs/knowledge_scheduler.log 2>&1 & | |
| * | |
| * 调度规则: | |
| * - 第1-4周: 每4小时运行,每次8篇 | |
| * - 第5-12周: 每6小时运行,每次5篇 | |
| * - 第13周+: 每12小时运行,每次2篇 | |
| */ | |
| import { fileURLToPath } from 'url'; | |
| import path from 'path'; | |
| import { spawn } from 'child_process'; | |
| import { getDb } from '../server/database.js'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| // ============ 配置 ============ | |
| // 调度配置 | |
| const SCHEDULE_CONFIG = { | |
| // 阶段定义 (按已完成文章数) | |
| phases: [ | |
| { name: 'phase1', maxArticles: 120, intervalHours: 4, batchSize: 8 }, | |
| { name: 'phase2', maxArticles: 300, intervalHours: 6, batchSize: 5 }, | |
| { name: 'phase3', maxArticles: Infinity, intervalHours: 12, batchSize: 2 }, | |
| ], | |
| // 失败任务重试 | |
| retryInterval: 3600000, // 1小时检查一次 | |
| maxRetries: 3, | |
| // 日志 | |
| logInterval: 600000, // 10分钟输出一次心跳 | |
| }; | |
| // ============ 状态管理 ============ | |
| let isRunning = false; | |
| let lastRunTime = null; | |
| let totalGenerated = 0; | |
| let totalFailed = 0; | |
| // 获取当前阶段 | |
| function getCurrentPhase() { | |
| const db = getDb(); | |
| const result = db.prepare(` | |
| SELECT COUNT(*) as count FROM knowledge_articles WHERE published = 1 | |
| `).get(); | |
| const articleCount = result?.count || 0; | |
| for (const phase of SCHEDULE_CONFIG.phases) { | |
| if (articleCount < phase.maxArticles) { | |
| return { ...phase, currentCount: articleCount }; | |
| } | |
| } | |
| return { ...SCHEDULE_CONFIG.phases[SCHEDULE_CONFIG.phases.length - 1], currentCount: articleCount }; | |
| } | |
| // 获取队列状态 | |
| function getQueueStatus() { | |
| const db = getDb(); | |
| try { | |
| const stats = db.prepare(` | |
| SELECT status, COUNT(*) as count | |
| FROM content_generation_queue | |
| GROUP BY status | |
| `).all(); | |
| return stats.reduce((acc, s) => { | |
| acc[s.status] = s.count; | |
| return acc; | |
| }, { pending: 0, generating: 0, completed: 0, failed: 0 }); | |
| } catch (e) { | |
| return { pending: 0, generating: 0, completed: 0, failed: 0, error: e.message }; | |
| } | |
| } | |
| // ============ 任务执行 ============ | |
| // 运行生成器 | |
| function runGenerator(phase, batchSize) { | |
| return new Promise((resolve, reject) => { | |
| console.log(`[${new Date().toISOString()}] 启动生成器: phase=${phase}, batch=${batchSize}`); | |
| const generatorPath = path.join(__dirname, 'knowledgeContentGenerator.js'); | |
| const child = spawn('node', [ | |
| generatorPath, | |
| `--phase=${phase}`, | |
| `--batch=${batchSize}`, | |
| ], { | |
| cwd: path.join(__dirname, '..'), | |
| stdio: 'pipe', | |
| }); | |
| let stdout = ''; | |
| let stderr = ''; | |
| child.stdout.on('data', (data) => { | |
| const output = data.toString(); | |
| stdout += output; | |
| process.stdout.write(output); // 实时输出 | |
| }); | |
| child.stderr.on('data', (data) => { | |
| const output = data.toString(); | |
| stderr += output; | |
| process.stderr.write(output); | |
| }); | |
| child.on('close', (code) => { | |
| if (code === 0) { | |
| // 解析完成数量 | |
| const completedMatch = stdout.match(/完成: (\d+)/); | |
| const failedMatch = stdout.match(/失败: (\d+)/); | |
| const completed = completedMatch ? parseInt(completedMatch[1]) : 0; | |
| const failed = failedMatch ? parseInt(failedMatch[1]) : 0; | |
| totalGenerated += completed; | |
| totalFailed += failed; | |
| resolve({ completed, failed }); | |
| } else { | |
| reject(new Error(`Generator exited with code ${code}: ${stderr}`)); | |
| } | |
| }); | |
| child.on('error', (error) => { | |
| reject(error); | |
| }); | |
| }); | |
| } | |
| // 重试失败任务 | |
| function retryFailedTasks() { | |
| const db = getDb(); | |
| try { | |
| const result = db.prepare(` | |
| UPDATE content_generation_queue | |
| SET status = 'pending' | |
| WHERE status = 'failed' AND retry_count < ? | |
| `).run(SCHEDULE_CONFIG.maxRetries); | |
| if (result.changes > 0) { | |
| console.log(`[${new Date().toISOString()}] 重置 ${result.changes} 个失败任务`); | |
| } | |
| } catch (e) { | |
| console.error(`[${new Date().toISOString()}] 重试失败任务出错:`, e.message); | |
| } | |
| } | |
| // ============ 调度逻辑 ============ | |
| async function runScheduledTask() { | |
| if (isRunning) { | |
| console.log(`[${new Date().toISOString()}] 跳过: 上一个任务仍在运行`); | |
| return; | |
| } | |
| isRunning = true; | |
| lastRunTime = new Date(); | |
| try { | |
| const phase = getCurrentPhase(); | |
| console.log(`\n${'='.repeat(60)}`); | |
| console.log(`[${new Date().toISOString()}] 开始调度任务`); | |
| console.log(`当前阶段: ${phase.name} | 已完成: ${phase.currentCount} 篇`); | |
| console.log(`本次批量: ${phase.batchSize} 篇`); | |
| console.log('='.repeat(60)); | |
| // 获取队列状态 | |
| const queueStatus = getQueueStatus(); | |
| console.log(`队列状态: 待处理=${queueStatus.pending}, 生成中=${queueStatus.generating}, 已完成=${queueStatus.completed}, 失败=${queueStatus.failed}`); | |
| if (queueStatus.pending === 0) { | |
| console.log('没有待处理任务,跳过本次运行'); | |
| isRunning = false; | |
| return; | |
| } | |
| // 运行生成器 | |
| const result = await runGenerator( | |
| phase.name === 'phase1' ? 1 : phase.name === 'phase2' ? 2 : 3, | |
| Math.min(phase.batchSize, queueStatus.pending) | |
| ); | |
| console.log(`\n[${new Date().toISOString()}] 任务完成: 生成 ${result.completed} 篇, 失败 ${result.failed} 篇`); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] 任务执行失败:`, error.message); | |
| } finally { | |
| isRunning = false; | |
| } | |
| } | |
| // 心跳日志 | |
| function logHeartbeat() { | |
| const phase = getCurrentPhase(); | |
| const queueStatus = getQueueStatus(); | |
| console.log(`[${new Date().toISOString()}] 心跳 | 阶段: ${phase.name} | 已发布: ${phase.currentCount}/300 | 待处理: ${queueStatus.pending} | 累计生成: ${totalGenerated} | 累计失败: ${totalFailed}`); | |
| } | |
| // ============ 主循环 ============ | |
| async function main() { | |
| console.log('═'.repeat(60)); | |
| console.log(' 知识内容调度器启动'); | |
| console.log('═'.repeat(60)); | |
| console.log(`启动时间: ${new Date().toISOString()}`); | |
| console.log(`PID: ${process.pid}`); | |
| console.log(''); | |
| // 初始检查 | |
| const phase = getCurrentPhase(); | |
| const queueStatus = getQueueStatus(); | |
| console.log(`当前阶段: ${phase.name}`); | |
| console.log(`已完成文章: ${phase.currentCount}`); | |
| console.log(`队列状态:`, queueStatus); | |
| console.log(`调度间隔: ${phase.intervalHours} 小时`); | |
| console.log(`每批数量: ${phase.batchSize} 篇`); | |
| console.log(''); | |
| // 立即运行一次 | |
| await runScheduledTask(); | |
| // 设置定时任务 | |
| const scheduleInterval = setInterval(async () => { | |
| const currentPhase = getCurrentPhase(); | |
| await runScheduledTask(); | |
| }, getCurrentPhase().intervalHours * 3600000); | |
| // 失败任务重试 | |
| setInterval(retryFailedTasks, SCHEDULE_CONFIG.retryInterval); | |
| // 心跳日志 | |
| setInterval(logHeartbeat, SCHEDULE_CONFIG.logInterval); | |
| // 优雅退出 | |
| process.on('SIGINT', () => { | |
| console.log(`\n[${new Date().toISOString()}] 收到 SIGINT,正在退出...`); | |
| clearInterval(scheduleInterval); | |
| process.exit(0); | |
| }); | |
| process.on('SIGTERM', () => { | |
| console.log(`\n[${new Date().toISOString()}] 收到 SIGTERM,正在退出...`); | |
| clearInterval(scheduleInterval); | |
| process.exit(0); | |
| }); | |
| console.log('调度器已启动,等待下次运行...\n'); | |
| } | |
| // 运行 | |
| main().catch(error => { | |
| console.error('调度器启动失败:', error); | |
| process.exit(1); | |
| }); | |