/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Db } from './db/db.js'; import type { AppConfig } from './config/app-config.js'; import type { AppContext } from './create-app.js'; import type { FastifyInstance } from 'fastify'; import { ScheduledTaskScheduler } from './schedule/scheduled-task.service.js'; import { readActiveBackendPort, readDeployLock } from './harness/core/deploy-lock.js'; import { WebhookDeliveryScheduler, DeliverySchedulerDbStore } from './notification/task/delivery.scheduler.js'; import { userMessagePreviewOf } from './notification/task/user-message-preview.js'; import { createSessionDetailUrlResolver } from './feishu/progress-card.js'; import { OutboundDeliveryScheduler } from './openapi/outbound-delivery.scheduler.js'; import { InboxCleanupScheduler } from './inbox/inbox.cleanup.js'; import { ScheduledTaskRunCleanupScheduler } from './schedule/run-cleanup.js'; import { EcpRenewScheduler } from './auth/ecp-renew.scheduler.js'; import { replayFeishuPendingMessage } from './feishu/pending-replay.js'; import { RuntimeCleanupScheduler } from './harness/runtime/runtime-cleanup-scheduler.js'; import { SubagentExecutionRecoveryService } from './harness/delegate/subagent-execution-recovery.service.js'; import { SubagentRecoveryCoordinator } from './harness/delegate/subagent-recovery-coordinator.js'; import { CrashRecoveryRunner } from './harness/core/crash-recovery-runner.js'; import { FeishuCardProgressListener } from './feishu/card-progress-listener.js'; import { DeployDrainWatcher } from './harness/core/deploy-drain-watcher.js'; import { createShutdownHandler } from './app-shutdown.js'; export interface LifecycleDeps { app: FastifyInstance; db: Db; cfg: AppConfig; ctx: AppContext; [key: string]: any; } export interface LifecycleResult { closeApp: () => Promise; } export function assembleLifecycle(deps: LifecycleDeps): LifecycleResult { const d = deps as any; const { app, db, cfg, ctx } = d; const scheduler = new ScheduledTaskScheduler(d.scheduledStore, d.scheduledService, () => { const active = readActiveBackendPort(cfg.app.harness.runtimeDir); if (active != null) return active === cfg.server.port; const lock = readDeployLock(cfg.app.harness.runtimeDir); if (lock != null && lock.oldPort === cfg.server.port) return false; return true; }); scheduler.start(); const deliveryScheduler = new WebhookDeliveryScheduler( new DeliverySchedulerDbStore(db), () => d.settingService.getNotificationTuningConfig(), d.notifCipher, d.senderRegistry, ); const sessionDetailUrl = createSessionDetailUrlResolver({ getSiteBaseUrl: () => d.settingService.getSiteBaseUrl(), }); deliveryScheduler.setContextProvider({ latestUserMessage: async (sessionId: number) => { const message = await d.sessionService.getLastUserMessage(sessionId); return userMessagePreviewOf(message?.content ?? null) || null; }, sessionDetailUrl: async (sessionId: number) => (await sessionDetailUrl(sessionId)) ?? null, }); deliveryScheduler.start(); const outboundDeliveryScheduler = new OutboundDeliveryScheduler({ deliveryRepo: d.outboundDeliveryRepo, subscriptionRepo: d.outboundSubscriptionRepo, cipher: d.notifCipher, // 共享单实例(create-app 构造):订阅测试投递与调度器真实投递同用 sender: d.genericWebhookSender, auditRecorder: { recordDeliveryFailed: async (input: any) => { let userId: number | null = null; if (input.subscriptionId != null) { const subscription = await d.outboundSubscriptionRepo.findById(input.subscriptionId).catch(() => null); userId = subscription?.userId ?? null; } await d.auditService.record({ action: 'EXECUTE', objectType: 'outbound.subscription', objectId: input.subscriptionId == null ? null : String(input.subscriptionId), userId, username: null, method: 'POST', path: `/v1/open/subscriptions/${input.subscriptionId}/deliveries`, ip: null, status: 502, success: 0, errorMessage: `出站投递终态失败 event=${input.event} url=${input.targetUrl} error=${input.error ?? ''}`, }).catch(() => undefined); }, }, execute: (fn: any) => d.agentExecutor.submit(fn), }); outboundDeliveryScheduler.start(); const inboxCleanupScheduler = new InboxCleanupScheduler(d.inboxCleanupStore); inboxCleanupScheduler.start(); const scheduledRunCleanup = new ScheduledTaskRunCleanupScheduler(d.scheduledStore); scheduledRunCleanup.start(); d.callLogService.start(); d.callLogCleanup.start(); const ecpRenewScheduler = new EcpRenewScheduler( d.ecpSessionRepo, () => d.settingService.getEcpConfig(), undefined, (token: string) => d.ecpSessionRepo.encryptToken(token), ); ecpRenewScheduler.start(); d.weixinMonitor.start(); d.feishuMonitor.start(); d.dingtalk.start(); void d.pendingBindingMessages.listRecoverable().then(async (pending: any[]) => { for (const message of pending) { if (ctx.pendingBindingProcessor == null) return; const claimed = await d.pendingBindingMessages.claim(message.state); if (claimed == null) continue; try { await replayFeishuPendingMessage(ctx.pendingBindingProcessor, claimed); await d.pendingBindingMessages.complete(claimed.state); } catch (error) { await d.pendingBindingMessages.release(claimed.state); console.error(`恢复飞书待绑定消息失败, state=${claimed.state}`, error); } } }).catch((error: unknown) => console.error('恢复飞书待绑定消息列表失败', error)); d.shellManager.startCleanup(); d.terminalManager.startCleanup(); const runtimeCleanup = new RuntimeCleanupScheduler( cfg.app.harness.runtimeDir, cfg.app.harness.cleanup, (_userId: number, sessionId: number) => d.wsHandler.hasExecutionClaim(sessionId), cfg.app.harness.skillViewsDir, ); runtimeCleanup.start(); const subagentExecutionRecovery = new SubagentExecutionRecoveryService( d.subagentMapper, d.sessionMap, d.sessionSvc, d.compactionSvc, d.definitionRegistry, (childSession: any, definition: any) => d.backgroundSubagentManager.buildSubContext(childSession, definition), d.agentLoop, d.visibility, d.localToolSessions, ); const subagentCoordinator = new SubagentRecoveryCoordinator( d.subagentMapper, subagentExecutionRecovery, d.subagentResultDelivery, d.sessionMap, d.sessionSvc, d.compactionSvc, d.agentExecutor, ); const crash = new CrashRecoveryRunner( d.sessionMap, d.sessionSvc, d.taskTerminal, d.harness, d.agentLoop, d.wsRegistry, d.activityService as never, d.activityHeartbeat, d.todoMapper, d.modelRepo as never, cfg.app.harness.runtimeDir, d.agentExecutor, async (sessionId: number, userId: number, phase: string) => { await d.scheduledService.reconcileRunsForSession(sessionId, phase).catch((error: unknown) => { console.warn(`定时任务运行记录在崩溃恢复后收敛失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); }); if (phase !== 'FAILED') { void d.wsHandler.autoConsumeQueue(sessionId, userId); await d.feishuProgressCardRepo.deleteBySessionId(sessionId).catch((error: unknown) => { console.warn(`清理飞书进度卡片映射失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); }); await d.feishuInboundHandler.drainNextIfPending(sessionId).catch((error: unknown) => { console.error(`飞书崩溃恢复后队列接力消费失败, sessionId=${sessionId}`, error); }); await d.dingtalk.onCrashFinished(sessionId, phase).catch((error: unknown) => { console.error(`钉钉崩溃恢复后队列接力消费失败, sessionId=${sessionId}`, error); }); } }, subagentCoordinator, async (sessionId: number) => { const dingtalkRecovered = await d.dingtalk.recoverProgress(sessionId); if (dingtalkRecovered != null) return new FeishuCardProgressListener(dingtalkRecovered.progress, dingtalkRecovered.roundOffset, cfg.feishu.bot.cardToolTickMs); const recovered = await d.createFeishuRecoveryProgress(sessionId); return recovered == null ? null : new FeishuCardProgressListener(recovered.progress, recovered.roundOffset, cfg.feishu.bot.cardToolTickMs); }, (sessionId: number) => d.agentLoop.getCancelFlag(sessionId) != null, cfg.server.port, d.localToolSessions, ); // 失败会话批量恢复:服务在 create-app 构造并注入路由, // 此处把崩溃恢复内核接给它——批量恢复与崩溃恢复共用同一续跑流程与并发防护。 d.failedSessionRecovery?.attachKernel(crash); void crash.run().catch((e: unknown) => console.error('Crash recovery failed', e)).then(async () => { await d.dingtalk.hydrate().catch((error: unknown) => console.error('钉钉入站队列启动恢复失败', error)); const sessionIds = await d.feishuTaskQueue.hydrate(); for (const sessionId of sessionIds) { await d.feishuInboundHandler.drainNextIfPending(sessionId).catch((error: unknown) => { console.error(`飞书队列启动恢复消费失败, sessionId=${sessionId}`, error); }); } }).catch((error: unknown) => console.error('飞书入站队列启动恢复失败', error)); crash.startPeriodicSweep(); const drainWatcher = new DeployDrainWatcher({ runtimeDir: cfg.app.harness.runtimeDir, selfPort: cfg.server.port, startedAtSec: Math.floor((Date.now() - process.uptime() * 1000) / 1000), shutdown: () => closeApp(), }); const closeApp = createShutdownHandler({ app, db, cfg, ctx, crash, drainWatcher, weixinMonitor: d.weixinMonitor, feishuMonitor: d.feishuMonitor, dingtalk: d.dingtalk, weixinInboundHandler: d.weixinInboundHandler, feishuInboundProcessor: d.feishuInboundProcessor, scheduler, deliveryScheduler, callLogService: d.callLogService, callLogCleanup: d.callLogCleanup, watcherScheduler: d.watcherScheduler, signalCheckCleanup: d.signalCheckCleanup, ecpRenewScheduler, shellManager: d.shellManager, terminalManager: d.terminalManager, ptyChannelHubs: d.ptyChannelHubs, ptyHubGeneration: d.ptyHubGeneration, containerRegistry: d.containerRegistry, channelServer: d.channelServer, runtimeCleanup, activityHeartbeat: d.activityHeartbeat, wsRegistry: d.wsRegistry, agentLoop: d.agentLoop, wsHandler: d.wsHandler, sessionService: d.sessionService, }); drainWatcher.start(); return { closeApp }; }