6k6g-v3 / src /worker /processor.ts
fanyubo
chore: 添加 BUILD_VERSION 启动日志验证构建生效
bd24c9a
Raw
History Blame Contribute Delete
17.5 kB
/**
* 任务处理器
* 使用 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<string, unknown>;
}): 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<string, unknown>; // ★ 附加元数据
draftContent?: string; // ★ 草案内容(HTML artifact)
skipPhase1?: boolean; // ★ 是否跳过 Phase 1
}
const QUEUE_KEYS = {
results: 'slow_task:results',
};
// ★★★ 并发模式:每个任务独立 executor,注册到 Map 供看门狗使用 ★★★
const taskExecutors = new Map<string, TaskExecutor>();
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<TaskExecutor> {
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<string, any> | null {
if (!raw) return null;
if (typeof raw === 'object') return raw as Record<string, any>;
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<string, any> = {}
): Promise<void> {
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<void> {
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<any> {
const executor = getGlobalExecutor();
if (!executor) {
return { error: 'Executor not initialized' };
}
// 从全局执行器获取状态(如果需要)
return { status: 'active', message: 'Executor available' };
}