/** * 任务处理器 * 使用 TaskExecutor 执行慢任务 * ★★★ 并发模式:每个任务独立 executor 实例 ★★★ * ★★★ 修复:上传到腾讯云 COS,设置真实 downloadUrl ★★★ */ import { getRedis } from '../lib/redis'; import { TaskParams, TaskResult, ProgressReport, DesignTaskType } from './task-executor'; import { ModelDispatcher } from '../lib/model-dispatcher'; import { NvidiaKeyRotator } from '../lib/nvidia-rotator'; import { LatencyTracker } from '../lib/latency-tracker'; import { isTaskAborted, clearAbortFlag, setGlobalExecutor, getGlobalExecutor } from './index'; import { TaskExecutor } from './task-executor'; import { uploadDocument, completeTask } from '../lib/storage-provider'; import { markdownToDocx } from '../lib/docx-engine'; import { generateOpenDesignAssetAsync } from '../lib/open-design-engine'; import { selectStyle } from '../lib/style-profiles'; import { sanitizeErrorMessage } from '../lib/error-sanitizer'; console.log('[STARTUP] processor loaded, BUILD: 20260522-v8-MIMO-FIX'); // ─── Server-side analytics ─── const ANALYTICS_ENDPOINT = 'https://www.5e1.com/api/analytics/event'; // ─── 退款接口(game.5e1.com)─── const GAME_API_BASE = 'https://game.5e1.com'; const INTERNAL_SECRET = process.env.INTERNAL_API_SECRET || ''; function recordEvent(options: { eventType: string; userId?: string | null; page?: string; meta?: Record; }): void { const { eventType, userId, page, meta = {} } = options; fetch(ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ eventType, product: 'game', page: page || null, userId: userId || null, meta: JSON.stringify(meta), }), signal: AbortSignal.timeout(5000), }).catch((err) => { console.warn('[Analytics] Failed to record event:', eventType, err); }); } interface SlowTask { taskId: string; userId: string; tier: string; taskType: string; status: string; chatContext?: Array<{ role: string; content: string }>; prompt?: string; // ★ 新增:直接 prompt 字段兼容 title?: string; // ★ 任务标题(由 submit API 生成) createdAt: number; callbackUrl?: string; // ★ 回调 URL metadata?: Record; // ★ 附加元数据 draftContent?: string; // ★ 草案内容(HTML artifact) skipPhase1?: boolean; // ★ 是否跳过 Phase 1 } const QUEUE_KEYS = { results: 'slow_task:results', }; // ★★★ 并发模式:每个任务独立 executor,注册到 Map 供看门狗使用 ★★★ const taskExecutors = new Map(); export function abortTask(taskId: string): boolean { const executor = taskExecutors.get(taskId); if (executor) { executor.abort(); console.log(`[Processor] 🛑 Aborted executor for task: ${taskId}`); return true; } return false; } function registerExecutor(taskId: string, executor: TaskExecutor): void { taskExecutors.set(taskId, executor); } function unregisterExecutor(taskId: string): void { taskExecutors.delete(taskId); } /** * 为每个任务创建独立的 executor 实例 * 共享同一个 NvidiaKeyRotator(轮询分发 key)和 Redis 连接 */ async function createTaskExecutor(): Promise { const redis = getRedis(); if (!redis) { throw new Error('Redis not available'); } const keyRotator = new NvidiaKeyRotator(); const latencyTracker = new LatencyTracker(redis); const dispatcher = new ModelDispatcher(keyRotator, latencyTracker, redis); return new TaskExecutor(dispatcher, redis); } // ─── 更新任务状态到 Redis ─── // ★★★ 安全 JSON 解析(处理 Upstash SDK 自动解析)★★★ function safeParseTaskState(raw: unknown): Record | null { if (!raw) return null; if (typeof raw === 'object') return raw as Record; try { return JSON.parse(raw as string); } catch { console.warn('[Processor] JSON parse failed for task state'); return null; } } async function updateTaskStatus( taskId: string, status: string, extra: Record = {} ): Promise { const redis = getRedis(); if (!redis) return; const existing = await redis.hget(QUEUE_KEYS.results, taskId); // ★★★ 如果没有现有数据,创建初始状态 ★★★ const state = safeParseTaskState(existing) || { taskId, status: 'queued', progress: 0, createdAt: Date.now(), }; const updated = { ...state, status, updatedAt: Date.now(), ...extra, }; await redis.hset(QUEUE_KEYS.results, { [taskId]: JSON.stringify(updated), }); } // 任务处理入口 export async function processTask(task: SlowTask): Promise { console.log('[Processor] Processing task:', task.taskId, 'type:', task.taskType); console.log('[Processor] Task prompt:', task.prompt?.slice(0, 50) || 'from chatContext'); // ★★★ 清除中止信号 ★★★ clearAbortFlag(); let executor: TaskExecutor | null = null; try { // ★★★ 并发模式:为每个任务创建独立 executor ★★★ executor = await createTaskExecutor(); registerExecutor(task.taskId, executor); console.log('[Processor] Executor created for task:', task.taskId); // ★★★ 注册进度回调 - 同时检查中止信号 ★★★ executor.onProgress(async (report: ProgressReport) => { // ★★★ 如果任务已被看门狗熔断,立即退出 ★★★ if (isTaskAborted()) { console.warn('[Processor] 任务已被看门狗熔断,停止执行'); throw new Error('Task aborted by watchdog'); } console.log('[Processor] Progress:', report.progress, '%', report.phase); await updateTaskStatus(task.taskId, report.status, { progress: report.progress, phase: report.phase, detail: report.detail, }); }); // 构建任务参数(兼容 chatContext 和 prompt 字段) const promptContent = task.chatContext?.map(m => m.content).join('\n') || task.prompt || ''; console.log('[Processor] Prompt content length:', promptContent.length); if (!promptContent) { throw new Error('No prompt content provided'); } // ★★★ 设计任务类型检测 ★★★ const DESIGN_TASK_TYPES: DesignTaskType[] = ['html_prototype', 'pitch_deck', 'ui_design_system']; const isDesign = DESIGN_TASK_TYPES.includes(task.taskType as DesignTaskType); // ★★★ 设计任务:随机选择视觉风格 ★★★ const designStyle = isDesign ? selectStyle() : undefined; const params: TaskParams = { prompt: promptContent, style: designStyle, tier: (task.tier as 'free' | 'starter' | 'pro' | 'enterprise') || 'free', taskType: task.taskType as TaskParams['taskType'], draftContent: task.draftContent, // ★ 透传草案内容 skipPhase1: task.skipPhase1, // ★ 透传跳过标记 }; // 执行任务 const result: TaskResult = await executor.execute(task.taskId, params); // ★★★ 保存生成内容到 Redis(备份)★★★ const redis = getRedis(); if (redis) { const contentKey = `slow_task:content:${task.taskId}`; await redis.set(contentKey, result.content, 'EX', 86400 * 7); // 7天过期 console.log('[Processor] Content saved to Redis:', contentKey, 'length:', result.content.length); } // ★★★ 上传到腾讯云 COS ★★★ // ★★★ 修复:初始化变量,确保始终有值 ★★★ let downloadUrl: string = `https://game.5e1.com/api/slow-task/content-raw/${task.taskId}`; let downloadSource: 'cos' | 'local' = 'local'; let expiresAt: number = Date.now() + 86400 * 7; try { const tier = (task.tier as 'free' | 'starter' | 'pro' | 'enterprise') || 'free'; // ★★★ 设计任务:使用 OpenDesignEngine 生成 ZIP/PPTX ★★★ if (isDesign && result.outputType) { console.log('[Processor] 设计任务,使用 OpenDesignEngine:', task.taskType, 'style:', designStyle?.name); const asset = await generateOpenDesignAssetAsync(task.taskType!, result.content, designStyle); console.log('[Processor] 设计资产生成完成, size:', asset.buffer.length, 'bytes, type:', asset.contentType); // 上传到 COS const uploadResult = await uploadDocument(task.taskId, asset.buffer, tier, undefined, { fileExtension: asset.fileExtension, contentType: asset.contentType, }); if (uploadResult.publicUrl.includes('/mock/')) { downloadSource = 'local'; console.warn('[Processor] ⚠ COS returned mock URL, using local fallback'); } else { downloadUrl = uploadResult.publicUrl; downloadSource = 'cos'; expiresAt = uploadResult.expiresAt; } console.log('[Processor] 设计资产已上传:', downloadUrl, 'expiresAt:', expiresAt); } else { // ★★★ 文档任务:生成 DOCX 文件 ★★★ const isFull = tier !== 'free'; const docTitle = result.title || task.title || (isFull ? '大师全案' : '灵感草案'); console.log('[Processor] 开始生成 DOCX...', docTitle, 'tier:', tier); const docxBuffer = await markdownToDocx(result.content, { title: docTitle, version: 'v1.0', confidentiality: isFull ? '机密文档' : '内部文档', tier, }); console.log('[Processor] DOCX generated, size:', docxBuffer.length, 'bytes'); // 上传到 COS console.log('[Processor] 开始上传到 COS, tier:', tier); const uploadResult = await uploadDocument(task.taskId, docxBuffer, tier); if (uploadResult.publicUrl.includes('/mock/')) { downloadSource = 'local'; console.warn('[Processor] ⚠ COS returned mock URL, using local fallback'); } else { downloadUrl = uploadResult.publicUrl; downloadSource = 'cos'; expiresAt = uploadResult.expiresAt; } console.log('[Processor] Uploaded to COS:', downloadUrl, 'expiresAt:', expiresAt); } } catch (uploadErr: any) { console.warn('[Processor] Upload/Design generation failed:', uploadErr.message); console.warn('[Processor] Error type:', uploadErr.constructor?.name); console.warn('[Processor] Error stack:', uploadErr.stack?.slice(0, 300)); // 失败时使用备用 URL(已初始化,无需重新赋值) } // ★★★ 更新最终结果 ★★★ // ★ 保留初始字段(title/taskType/tier/userId),防止 updateTaskStatus 丢失 // ★★★ 优先使用 LLM 生成的标题(result.title),其次用 submit 传入的标题 ★★★ const finalTitle = result.title || task.title || ''; console.log('[Processor] 最终标题:', finalTitle, '(LLM:', result.title, '| submit:', task.title, ')'); await updateTaskStatus(task.taskId, 'completed', { progress: 100, completedAt: Date.now(), sectionsGenerated: result.sectionsGenerated, duration: result.duration, downloadUrl, // ★ 使用真实 COS URL 或备用 URL downloadSource, // ★ OPT-05: 标记下载来源 expiresAt, qualityGrade: result.phases?.review?.model ? 'A' : 'B', phases: result.phases, // ★ 补回初始字段 title: finalTitle, taskType: task.taskType, tier: task.tier, userId: task.userId, createdAt: task.createdAt, }); console.log('[Processor] Task completed:', task.taskId, 'duration:', result.duration, 'ms', 'downloadUrl:', downloadUrl); // ★ 追踪:任务完成事件 recordEvent({ eventType: 'slow_task_complete', userId: task.userId, page: '/api/slow-task/callback', meta: { taskType: task.taskType, totalTimeMs: result.duration, }, }); // ★★★ 回调通知:任务完成后 POST 结果到 callbackUrl ★★★ try { const existingState = await redis?.hget(QUEUE_KEYS.results, task.taskId); const state = existingState ? (typeof existingState === 'string' ? JSON.parse(existingState) : existingState) : {}; const callbackUrl = state.callbackUrl || task.callbackUrl; if (callbackUrl) { console.log('[Processor] Sending callback to:', callbackUrl); const callbackBody = { taskId: task.taskId, taskType: task.taskType, status: 'completed', content: result.content, downloadUrl, qualityGrade: result.phases?.review?.model ? 'A' : 'B', duration: result.duration, sectionsGenerated: result.sectionsGenerated, metadata: state.metadata || {}, }; fetch(callbackUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(callbackBody), signal: AbortSignal.timeout(15000), }).then(r => { console.log('[Processor] Callback response:', r.status); }).catch(cbErr => { console.warn('[Processor] Callback failed:', cbErr.message); }); } } catch (cbErr) { console.warn('[Processor] Callback error:', cbErr); } // 重置执行器状态并注销 executor.reset(); unregisterExecutor(task.taskId); } catch (err: any) { // ★★★ 增强错误捕获 - 确保错误信息正确序列化 ★★★ const rawErrorMessage = typeof err === 'string' ? err : (err?.message ? err.message : (typeof err === 'object' ? JSON.stringify(err) : 'Unknown error')); const errorStack = err?.stack?.split('\n').slice(0, 5).join('\n') || 'No stack trace'; console.error('[Processor] Task processing failed:', rawErrorMessage); console.error('[Processor] Error type:', typeof err); console.error('[Processor] Error stack:', errorStack); // 如果是JSON解析错误,尝试记录原始响应 if (rawErrorMessage.includes('JSON') && err?.rawResponse) { console.error('[Processor] Raw response that caused JSON error:', err.rawResponse); } // ★★★ 脱敏清洗:确保不暴露模型名、API Key 等技术细节 ★★★ const errorMessage = sanitizeErrorMessage(rawErrorMessage); try { await updateTaskStatus(task.taskId, 'failed', { errorMessage: errorMessage, errorStack: errorStack, completedAt: Date.now(), // ★ 补回初始字段 title: task.title, taskType: task.taskType, tier: task.tier, userId: task.userId, createdAt: task.createdAt, }); // ★ 追踪:任务失败事件 recordEvent({ eventType: 'slow_task_fail', userId: task.userId, page: '/api/slow-task/callback', meta: { taskType: task.taskType, error: errorMessage, }, }); // ★★★ 回调通知:任务失败时也通知 callbackUrl ★★★ try { const redis = getRedis(); const existingState = redis ? await redis.hget(QUEUE_KEYS.results, task.taskId) : null; const state = existingState ? (typeof existingState === 'string' ? JSON.parse(existingState) : existingState) : {}; const callbackUrl = state.callbackUrl || task.callbackUrl; if (callbackUrl) { fetch(callbackUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: task.taskId, taskType: task.taskType, status: 'failed', errorMessage, }), signal: AbortSignal.timeout(10000), }).catch(() => {}); } } catch {} // ★★★ 退款:任务失败时退还配额和 Credits ★★★ try { const isLite = task.metadata?.planType === 'lite'; const refundEndpoint = `${GAME_API_BASE}/api/slow-task/refund`; fetch(refundEndpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${INTERNAL_SECRET}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ taskId: task.taskId, userId: task.userId, taskType: task.taskType, isLite, preDeductedCredits: task.metadata?.preDeductedCredits || 0, }), signal: AbortSignal.timeout(10000), }).then(r => { console.log('[Processor] Refund response:', r.status); }).catch(refundErr => { console.warn('[Processor] Refund request failed:', refundErr.message); }); } catch {} } catch (updateErr) { console.error('[Processor] Failed to update task status:', updateErr); } // ★★★ 注销 executor(无论成功失败)★★★ if (executor) { unregisterExecutor(task.taskId); } } } // 获取调度器状态摘要 export async function getDispatcherSummary(): Promise { const executor = getGlobalExecutor(); if (!executor) { return { error: 'Executor not initialized' }; } // 从全局执行器获取状态(如果需要) return { status: 'active', message: 'Executor available' }; }