/** * 使命必达执行器 - Mission-Critical Task Executor * * ★★★ 核心特性 ★★★ * 1. 智能模型切换:超时自动切换到下一个模型,不等待看门狗 * 2. 速度优先:记录快速模型,下次优先使用 * 3. 无限重试:模型池轮询,直到找到响应快的模型 * 4. 使命必达:绝不放弃,直到任务完成 * 5. ★★★ 任务 3: 模型感知适配 - 自动折叠 System Role、裁剪 Context ★★★ */ import pLimit from 'p-limit'; import Redis from 'ioredis'; import { ModelDispatcher } from '../lib/model-dispatcher'; import { getModelMeta, TaskPhase, MODEL_POOL } from '../lib/model-registry'; // ★★★ 任务 3: 导入适配器 ★★★ import { adaptPayload, sniffErrorAndLearn, getEffectiveModelMeta, ChatMessage, } from '../lib/payload-adapter'; import { getDesignPrompt, getStyledPrompt } from '../lib/design-prompt-loader'; import type { StyleProfile } from '../lib/style-profiles'; import { runOpenGamePipeline } from '../lib/opengame-pipeline'; import { sanitizeErrorMessage } from '../lib/error-sanitizer'; console.log('[STARTUP] task-executor loaded, BUILD: 20260523-v9-ANTI-SLOP-V2'); export type DesignTaskType = 'html_prototype' | 'pitch_deck' | 'ui_design_system'; export type OpenGameTaskType = 'opengame_generation'; export interface TaskParams { prompt: string; sections?: number; style?: StyleProfile; maxTokens?: number; draftContent?: string; skipPhase1?: boolean; tier?: 'free' | 'starter' | 'pro' | 'enterprise'; taskType?: 'full_plan' | 'sales_manual' | 'world_setting' | 'competitor_analysis' | DesignTaskType | OpenGameTaskType; } export interface TaskOutline { title: string; sections: Array<{ title: string; points: string[]; }>; metadata?: Record; } export interface TaskResult { content: string; outline: TaskOutline; sectionsGenerated: number; duration: number; title?: string; // ★ LLM 生成的文档标题(15字以内) phases: { planning: { duration: number; model: string; attempts: number }; expansion: { duration: number; model: string; sections: number; attempts: number }; review: { duration: number; model: string; attempts: number }; }; totalAttempts: number; // 总尝试次数 fastModelsUsed: string[]; // 使用的快速模型列表 outputType?: 'docx' | 'zip' | 'pptx'; fileExtension?: string; contentType?: string; } export interface ProgressReport { taskId: string; status: 'planning' | 'writing' | 'reviewing' | 'completed' | 'failed'; progress: number; phase: string; detail?: string; updatedAt: number; modelSwitch?: string; // 模型切换信息 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ★★★ 日本服务器极速跳板模式 ★★★ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ★★★ 任务 1: 使用日本跳板替代直连 NVIDIA ★★★ const PROXY_ENDPOINT = process.env.LLM_PROXY_ENDPOINT || 'https://game.5e1.com/api/internal/llm-proxy'; const PROXY_SECRET = process.env.INTERNAL_API_SECRET || ''; const CONCURRENCY_LIMIT = 2; // ★★★ 设计任务类型检测 ★★★ const DESIGN_TASK_TYPES: DesignTaskType[] = ['html_prototype', 'pitch_deck', 'ui_design_system']; function isDesignTask(taskType?: string): taskType is DesignTaskType { return DESIGN_TASK_TYPES.includes(taskType as DesignTaskType); } function getDesignTaskOutputMeta(taskType: DesignTaskType): { outputType: 'zip' | 'pptx'; fileExtension: string; contentType: string } { if (taskType === 'pitch_deck') { return { outputType: 'pptx', fileExtension: 'pptx', contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; } return { outputType: 'zip', fileExtension: 'zip', contentType: 'application/zip', }; } // ★★★ 超时策略:给模型充足生成时间 ★★★ const MODEL_TIMEOUT_MS = 45000; // 45秒总超时(含 TTFB + token 生成,mimo-v2.5 planning 需要 20-30s) const MAX_MODEL_ATTEMPTS = 5; const FAST_MODEL_THRESHOLD = 5000; const SLOW_MODEL_BLACKLIST_MS = 120000; // Redis Key 用于存储快速模型优先级 const FAST_MODEL_KEY = 'slow_task:fast_models'; export class TaskExecutor { private dispatcher: ModelDispatcher; private redis: Redis; private abortFlag: boolean = false; private progressCallback: ((report: ProgressReport) => void) | null = null; // 心跳状态 private lastProgress: number = 0; private lastPhase: string = 'init'; private lastDetail: string = ''; // AbortController private abortController: AbortController | null = null; // ★★★ 新增:快速模型缓存 ★★★ private fastModelCache: Map = new Map(); private slowModelBlacklist: Map = new Map(); // modelId -> blacklistUntil constructor(dispatcher: ModelDispatcher, redis: Redis) { this.dispatcher = dispatcher; this.redis = redis; // 加载快速模型缓存 this.loadFastModelCache(); } /** * 从 Redis 加载快速模型缓存 */ private async loadFastModelCache(): Promise { try { const data = await this.redis.hgetall(FAST_MODEL_KEY); if (data) { for (const [modelId, stats] of Object.entries(data)) { const parsed = typeof stats === 'string' ? JSON.parse(stats) : stats; this.fastModelCache.set(modelId, { avgTtfb: parsed.avgTtfb || 5000, lastSuccess: parsed.lastSuccess || Date.now() }); } console.log(`[MissionExecutor] 📊 加载快速模型缓存: ${this.fastModelCache.size} 个模型`); } } catch (err) { console.warn('[MissionExecutor] 加载快速模型缓存失败:', err); } } /** * 记录快速模型 */ private async recordFastModel(modelId: string, ttfb: number): Promise { if (ttfb < FAST_MODEL_THRESHOLD) { const existing = this.fastModelCache.get(modelId); const avgTtfb = existing ? (existing.avgTtfb + ttfb) / 2 : ttfb; this.fastModelCache.set(modelId, { avgTtfb, lastSuccess: Date.now() }); // 写入 Redis 持久化 try { await this.redis.hset(FAST_MODEL_KEY, { [modelId]: JSON.stringify({ avgTtfb, lastSuccess: Date.now() }) }); console.log(`[MissionExecutor] ⚡ 记录快速模型: ${modelId} (avg TTFB: ${avgTtfb}ms)`); } catch (err) { console.warn('[MissionExecutor] 记录快速模型失败:', err); } } } /** * 检查模型是否在黑名单中 */ private isModelBlacklisted(modelId: string): boolean { const blacklistUntil = this.slowModelBlacklist.get(modelId); if (blacklistUntil && Date.now() < blacklistUntil) { return true; } // 黑名单过期,移除 if (blacklistUntil) { this.slowModelBlacklist.delete(modelId); } return false; } /** * 将慢模型加入临时黑名单 */ private blacklistSlowModel(modelId: string): void { this.slowModelBlacklist.set(modelId, Date.now() + SLOW_MODEL_BLACKLIST_MS); console.log(`[MissionExecutor] 🚫 暂时黑名单慢模型: ${modelId} (${SLOW_MODEL_BLACKLIST_MS / 1000}秒)`); } /** * ★★★ 核心方法:使命必达的模型调用 ★★★ * 特性: * - 单次超时45秒,超时立即切换下一个模型 * - 每个阶段最多尝试5个模型 * - 记录快速模型,下次优先使用 * - 黑名单慢模型2分钟 */ private async callNIMMissionCritical( taskId: string, phase: TaskPhase, messages: Array<{ role: string; content: string }>, maxTokens: number, systemPrompt?: string ): Promise<{ content: string; modelId: string; ttfb: number; attempts: number }> { const startTime = Date.now(); let attempts = 0; let lastError: Error | null = null; this.dispatcher.setPhase(phase); // ★★★ 最多尝试 MAX_MODEL_ATTEMPTS 个不同模型 ★★★ while (attempts < MAX_MODEL_ATTEMPTS) { attempts++; // 获取下一个可用模型(跳过黑名单) let modelId: string | null = null; let tryCount = 0; const maxTry = 10; while (!modelId && tryCount < maxTry) { const candidate = this.dispatcher.getNextModel(); if (!candidate) { console.warn(`[MissionExecutor] 无可用模型,重置健康状态`); this.dispatcher.resetAllHealth(); continue; } // 检查黑名单 if (!this.isModelBlacklisted(candidate)) { modelId = candidate; } else { console.log(`[MissionExecutor] 跳过黑名单模型: ${candidate}`); } tryCount++; } if (!modelId) { // 所有模型都在黑名单,清空黑名单强制使用 this.slowModelBlacklist.clear(); modelId = this.dispatcher.getNextModel(); if (!modelId) { throw new Error('所有模型池耗尽,无法继续'); } } const apiKey = this.dispatcher.getApiKey(); const meta = getModelMeta(modelId); console.log(`[MissionExecutor] 🎯 第 ${attempts} 次尝试: ${modelId} (${phase}阶段)`); await this.updateProgress(taskId, this.lastProgress, this.lastPhase, `${phase}阶段尝试 ${attempts}/${MAX_MODEL_ATTEMPTS}: ${modelId}`); // ★★★ 单次调用超时45秒 ★★★ const callStartTime = Date.now(); let heartbeatInterval: ReturnType | null = null; // ★★★ 定义在外层,便于 catch 访问 ★★★ try { // ★★★ 修复:创建 AbortController 用于 fetch 和看门狗 ★★★ this.abortController = new AbortController(); this.abortFlag = false; // ★★★ 关键修复:使用 AbortSignal.timeout 覆盖整个请求周期 ★★★ // AbortSignal.timeout 会在 fetch + response.json() 整个过程中超时 // 这是 Node.js 18+ 特性,比手动 setTimeout 更可靠 const timeoutSignal = AbortSignal.timeout(MODEL_TIMEOUT_MS); // 心跳定时器(每10秒更新进度) heartbeatInterval = setInterval(async () => { if (this.abortFlag) { if (heartbeatInterval) clearInterval(heartbeatInterval); return; } const elapsed = Math.floor((Date.now() - callStartTime) / 1000); await this.updateProgress(taskId, this.lastProgress, this.lastPhase, `${phase}阶段: ${modelId} 响应中 (${elapsed}秒)`); }, 10000); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ★★★ 任务 3: 模型感知适配 ★★★ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 1. 获取有效模型元数据(合并静态 + 学习数据) const effectiveMeta = await getEffectiveModelMeta(this.redis, modelId); // 2. 构建原始消息数组 const rawMessages: ChatMessage[] = systemPrompt ? [{ role: 'system', content: systemPrompt }, ...messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }))] : messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })); // 3. 使用适配器自动适配 const adapted = adaptPayload(modelId, rawMessages, { maxTokens }); // 4. 如果有适配行为,打印日志 if (adapted.adapted) { console.log(`[MissionExecutor] 🔧 ${modelId} 适配: ${adapted.adaptations.join('; ')}`); await this.updateProgress(taskId, this.lastProgress, this.lastPhase, `${phase}阶段: ${modelId} 已适配 (${adapted.adaptations.length}项)`); } const finalMaxTokens = adapted.maxTokens; const finalMessages = adapted.messages; // ★★★ 任务 2: 使用跳板模式 ★★★ // 不再直连 NVIDIA,而是通过日本服务器跳板 console.log(`[MissionExecutor] 🚀 使用跳板: ${PROXY_ENDPOINT}`); // ★★★ 硬超时:Promise.race 覆盖 fetch + response.json() 整个生命周期 ★★★ // AbortSignal.timeout 只控制连接/TTFB,不控制 token 生成速度 // Promise.race 确保总时间不超过 MODEL_TIMEOUT_MS const fetchAndParse = async () => { const response = await fetch(PROXY_ENDPOINT, { method: 'POST', headers: { 'Authorization': `Bearer ${PROXY_SECRET}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelId, messages: finalMessages, max_tokens: finalMaxTokens, stream: false }), signal: timeoutSignal }); if (!response.ok) { const errText = await response.text(); throw { status: response.status, body: errText, isHttpError: true }; } return response.json() as Promise<{ choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>; _proxy_meta?: { ttfb: number }; }>; }; const hardTimeout = new Promise((_, reject) => { setTimeout(() => reject(new Error('MODEL_HARD_TIMEOUT')), MODEL_TIMEOUT_MS); }); const data = await Promise.race([fetchAndParse(), hardTimeout]); // ★★★ 跳板返回错误处理 + 任务 3: 错误嗅探与学习 ★★★ // (HTTP 错误已在 fetchAndParse 内部抛出) const content = data.choices?.[0]?.message?.content || ''; if (!content) { const rLen = data.choices?.[0]?.message?.reasoning_content?.length || 0; console.warn(`[MissionExecutor] ⚠️ ${modelId} content 为空! reasoning_content=${rLen} tokens, 可能 max_tokens 不足或缺少 system prompt`); } const ttfb = data._proxy_meta?.ttfb || (Date.now() - callStartTime); // ★★★ 清除心跳定时器 ★★★ if (heartbeatInterval) clearInterval(heartbeatInterval); // ★★★ 成功!记录快速模型 ★★★ await this.recordFastModel(modelId, ttfb); await this.dispatcher.reportSuccess(modelId, ttfb, content.length / 4, Date.now() - callStartTime); console.log(`[MissionExecutor] ✅ 成功: ${modelId} TTFB=${ttfb}ms 尝试次数=${attempts}`); return { content, modelId, ttfb, attempts }; } catch (err: any) { if (heartbeatInterval) clearInterval(heartbeatInterval); const elapsed = Date.now() - callStartTime; // ★★★ 区分超时、HTTP 错误和其他错误 ★★★ const isTimeout = err.message === 'MODEL_HARD_TIMEOUT' || err.name === 'AbortError' || err.name === 'TimeoutError' || elapsed >= MODEL_TIMEOUT_MS - 1000; if (isTimeout) { console.warn(`[MissionExecutor] ⏱️ ${modelId} 超时 (${elapsed}ms),加入黑名单,切换下一个`); this.blacklistSlowModel(modelId); await this.dispatcher.reportFailure(modelId, 504, 'Timeout'); lastError = new Error(`模型 ${modelId} 超时 (${elapsed}ms)`); } else if (err.isHttpError) { // ★★★ HTTP 错误:嗅探学习 ★★★ console.error(`[跳板报错] 模型: ${modelId}, 状态码: ${err.status}, 内容: ${err.body?.slice(0, 200)}`); const sniffResult = await sniffErrorAndLearn(this.redis, modelId, err.body || ''); if (sniffResult.shouldRetry && attempts < MAX_MODEL_ATTEMPTS) { console.log(`[MissionExecutor] 🔄 ${modelId} 自愈重试 (已学习适配)`); attempts--; if (sniffResult.newParams) maxTokens = sniffResult.newParams.maxTokens; continue; } await this.dispatcher.reportFailure(modelId, err.status || 500, err.body?.slice(0, 100) || 'Unknown'); lastError = new Error(`HTTP ${err.status}: ${(err.body || '').slice(0, 200)}`); } else { console.warn(`[MissionExecutor] ❌ ${modelId} 错误: ${err.message}`); await this.dispatcher.reportFailure(modelId, 500, err.message); lastError = err; } // 继续尝试下一个模型 console.log(`[MissionExecutor] 🔄 切换到下一个模型...`); } } // 所有模型都失败 throw new Error(sanitizeErrorMessage( `使命必达失败: ${phase}阶段尝试了 ${attempts} 个模型均超时/失败。最后错误: ${lastError?.message}` )); } /** * 执行慢任务完整流程 */ async execute(taskId: string, params: TaskParams): Promise { const startTime = Date.now(); const phases = { planning: { duration: 0, model: '', attempts: 0 }, expansion: { duration: 0, model: '', sections: 0, attempts: 0 }, review: { duration: 0, model: '', attempts: 0 } }; let totalAttempts = 0; const fastModelsUsed: string[] = []; this.abortController = new AbortController(); this.abortFlag = false; // ★ OPT-02: 兜底 — 当 prompt 为空但 draftContent 存在时,用 draftContent 作为 prompt if (!params.prompt && params.draftContent) { params.prompt = params.draftContent; console.log(`[TaskExecutor] ⚡ Prompt 为空,使用 draftContent 作为 fallback (${params.prompt.length} chars)`); } // ★★★ DRY_RUN 急速测试模式 ★★★ if (params.prompt && params.prompt.includes('[DRY_RUN]')) { console.log('[MissionExecutor] 🚀 DRY_RUN 模式'); await this.updateProgress(taskId, 50, 'writing', 'DRY_RUN 测试...'); await new Promise(resolve => setTimeout(resolve, 3000)); const fakeMarkdown = `# DRY_RUN 测试文档 任务 ID: ${taskId} 测试时间: ${new Date().toISOString()} 使命必达机制验证成功!`; await this.redis.set(`slow_task:content:${taskId}`, fakeMarkdown, 'EX', 86400 * 7); await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify({ taskId, status: 'completed', progress: 100, phase: 'completed', detail: 'DRY_RUN 完成', updatedAt: Date.now(), // downloadUrl 由 processor.ts 上传 COS 后设置 qualityGrade: 'S', sectionsGenerated: 1, duration: 3000 }) }); return { content: fakeMarkdown, outline: { title: 'DRY_RUN', sections: [{ title: 'Test', points: ['test'] }] }, sectionsGenerated: 1, duration: 3000, phases, totalAttempts: 1, fastModelsUsed: ['dry_run'], title: 'DRY_RUN', }; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // OpenGame 管线任务 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ if (params.taskType === 'opengame_generation') { return this.executeOpenGame(taskId, params); } try { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Phase 1: 结构规划 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ await this.updateProgress(taskId, 5, 'planning', '开始规划大纲...'); const planningStart = Date.now(); let outline: TaskOutline; let llmTitle = ''; // ★ LLM 生成的文档标题 // ★★★ 修复:draftContent 为空时,fallback 到 prompt(含 chatContext 中的 HTML artifact) ★★★ const draftSource = params.draftContent || ''; // 检测是否为草案模式:有 draftContent,或 prompt 包含 HTML/结构化内容 const hasHtmlContent = params.prompt && (/]*>/i.test(params.prompt) || /]/i.test(params.prompt) || /]/i.test(params.prompt)); const isDraftMode = (draftSource.length > 0 && params.skipPhase1) || hasHtmlContent; const effectiveDraft = draftSource.length > 0 ? draftSource : (hasHtmlContent ? params.prompt : ''); if (isDraftMode && effectiveDraft.length > 0) { // ★★★ 关键:先清洗 HTML,移除 style/script/head 内容 ★★★ const cleanedContent = this.cleanHtmlContent(effectiveDraft); // 先尝试结构化提取 outline = this.extractOutlineFromDraft(effectiveDraft); phases.planning.model = 'draft-anchor'; phases.planning.attempts = 1; // ★ 诊断数据 const draftDebug: Record = { promptLen: params.prompt?.length || 0, draftSourceLen: draftSource.length, effectiveDraftLen: effectiveDraft.length, cleanedContentLen: cleanedContent.length, cleanedContentPreview: cleanedContent.slice(0, 200), hasHtmlContent, isDraftMode, extractedSections: outline.sections.length, extractedTitle: outline.title, firstSectionTitle: outline.sections[0]?.title || '', buildTag: 'v9-anti-slop-v2', llmOutlineAttempted: false, llmOutlineSuccess: false, llmTitleAttempted: false, llmTitleSuccess: false, }; (phases.planning as any).draftDebug = draftDebug; // ★★★ 如果结构化提取只得到泛化 fallback,用 LLM 从实际内容生成大纲 ★★★ const isGenericOutline = outline.sections.length === 1 && outline.sections[0].title === '主体内容'; if (isGenericOutline) { console.log('[MissionExecutor] 结构化提取失败,使用 LLM 从实际内容生成大纲...'); draftDebug.llmOutlineAttempted = true; try { const isPaid = params.tier && params.tier !== 'free'; const contentForLLM = cleanedContent.slice(0, 4000); const outlinePrompt = `你是一位文档分析专家。请分析以下内容,提取核心主题并生成文档大纲。 内容: ${contentForLLM} 严格输出纯 JSON,禁止包含其他文字或代码块标记: { "title": "文档标题(15字以内,精准有力,必须体现具体内容主题)", "sections": [ { "title": "章节标题", "points": ["要点1", "要点2", "要点3"] } ] } 要求: - 章节数量:${isPaid ? '5-8' : '4-6'} 个 - 每章节 3-5 个要点 - 标题必须反映内容的具体主题,禁止使用"主体内容""概述"等泛化标题`; const outlineResult = await this.callNIMMissionCritical( taskId, 'planning', [{ role: 'user', content: outlinePrompt }], 4000, '你是一位文档分析专家。只输出纯 JSON,不要任何多余文字。' ); draftDebug.llmOutlineRaw = outlineResult.content.slice(0, 500); draftDebug.llmOutlineModel = outlineResult.modelId; // 安全解析 JSON const jsonStr = outlineResult.content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim(); let parsed: any = null; try { parsed = JSON.parse(jsonStr); } catch { const m = jsonStr.match(/\{[\s\S]*\}/); if (m) { try { parsed = JSON.parse(m[0]); } catch {} } } if (parsed && parsed.sections && parsed.sections.length > 0) { outline = { title: parsed.title || outline.title, sections: parsed.sections.map((s: any) => ({ title: s.title || '未命名章节', points: Array.isArray(s.points) ? s.points : ['详细阐述'], })), metadata: { source: 'draft-llm' }, }; llmTitle = parsed.title || ''; draftDebug.llmOutlineSuccess = true; draftDebug.llmSections = outline.sections.length; draftDebug.llmOutlineTitle = llmTitle; console.log('[MissionExecutor] LLM 大纲生成成功:', outline.title, '章节:', outline.sections.length); } else { draftDebug.llmOutlineParseError = 'JSON parsed but no sections found'; console.warn('[MissionExecutor] LLM 大纲 JSON 解析成功但无 sections:', JSON.stringify(parsed)?.slice(0, 200)); } } catch (outlineErr: any) { draftDebug.llmOutlineError = outlineErr.message?.slice(0, 200); console.warn('[MissionExecutor] LLM 大纲生成失败:', outlineErr.message); } } // ★★★ 标题生成:如果 LLM 大纲没有产出标题,调用专门的标题生成 ★★★ if (!llmTitle) { draftDebug.llmTitleAttempted = true; try { const titleResult = await this.callNIMMissionCritical( taskId, 'planning', [{ role: 'user', content: `请为以下文档内容生成一个精准有力的标题(15字以内,直接输出标题文字,不要引号或任何标记):\n\n${cleanedContent.slice(0, 1000)}` }], 2000, '你是文档标题专家。只输出一个标题,15字以内,精准有力,不要任何多余文字。' ); const rawTitle = titleResult.content.replace(/["'"「」【】\s\n\r]/g, '').slice(0, 15); if (rawTitle.length > 0) { llmTitle = rawTitle; draftDebug.llmTitleSuccess = true; draftDebug.llmTitleResult = llmTitle; draftDebug.llmTitleModel = titleResult.modelId; console.log('[MissionExecutor] LLM 生成标题(草案模式):', llmTitle); } else { draftDebug.llmTitleEmpty = true; console.warn('[MissionExecutor] LLM 标题返回空'); } } catch (titleErr: any) { draftDebug.llmTitleError = titleErr.message?.slice(0, 200); console.warn('[MissionExecutor] 标题生成失败:', titleErr.message); } // ★★★ 最终兜底:从内容首行提取标题 ★★★ if (!llmTitle) { const firstLine = cleanedContent.split(/[。\n]/).find(l => l.trim().length > 3) || ''; llmTitle = firstLine.replace(/[^a-zA-Z0-9一-鿿·\-| ]/g, '').trim().slice(0, 15); draftDebug.heuristicTitle = llmTitle; console.log('[MissionExecutor] 启发式标题:', llmTitle); } } } else { const isPaid = params.tier && params.tier !== 'free'; const planningSystemPrompt = isPaid ? `你是腾讯/网易级资深系统策划,正在为「大师全案」生成文档大纲。 【反 AI 废话协议】 - 禁止学术名词堆砌(沉没成本/损失厌恶等术语全篇最多出现1次) - 禁止名人名言、煽情愿景 - 章节标题必须具体有力,禁止"概述""简介""背景"等虚词 - 每个要点必须是可执行的具体动作或可量化的数据指标 大纲格式(严格输出纯 JSON): { "title": "文档标题(15字以内,体现具体主题,禁止'完整策划案''大师全案'等泛化词)", "subtitle": "副标题(20字以内,说明核心价值)", "sections": [ { "title": "章节标题", "points": ["要点1", "要点2", "要点3", "要点4", "要点5"] } ] } 结构要求: - 章节数量:6-8 个,逻辑递进 - 每章节 4-6 个要点,每个要点必须具体到可执行 - 必须包含的模块(按顺序): 1. 现状诊断(含具体数据指标和问题定位) 2. 核心机制设计(含数据结构、接口定义、边界情况) 3. 用户流程与交互(含客户端-服务端通信流程) 4. 数值与经济系统(含数值表、平衡公式、付费转化节点) 5. 风险与异常处理(含 TOP3 风险 + 具体规避方案) 6. 验收标准(含可测量 KPI 和测试用例) - 要点示例(必须达到此粒度): ✓ "用户数据表含 userId/giftId/status/purchasedAt 五个核心字段" ✓ "支付回调超时 30s 触发定时补偿任务,24h 内自动对账" ✓ "首充转化率目标 42%,基于沉没成本机制行业基准推导" ✗ "分析用户需求"(太抽象) ✗ "制定运营策略"(太泛化)` : `你是资深系统策划,正在为「灵感草案」生成文档大纲。 【反 AI 废话协议】禁止学术名词堆砌、名人名言、模糊词。章节标题必须具体。 大纲格式(严格输出纯 JSON): { "title": "文档标题(10字以内,体现具体主题,禁止'灵感草案''草案'等泛化词)", "subtitle": "副标题(简短描述核心方向)", "sections": [ { "title": "章节标题", "points": ["要点1", "要点2", "要点3"] } ] } 结构要求: - 章节数量:4-5 个,抓住核心 - 每章节 3-4 个要点,每个要点具体可执行 - 必须覆盖:核心机制 → 关键数据/数值 → 执行路径 → 风险与注意事项 - 要点必须有具体数字或可执行动作,禁止泛化罗列 - 标题示例:"独立游戏首充系统设计"(好) vs "变现策略分析"(差)`; const planningResult = await this.callNIMMissionCritical( taskId, 'planning', [{ role: 'user', content: params.prompt }], 4000, planningSystemPrompt ); // 解析大纲 try { const jsonMatch = planningResult.content.match(/\{[\s\S]*\}/); outline = jsonMatch ? JSON.parse(jsonMatch[0]) : { title: '生成文档', sections: [ { title: '概述', points: ['背景介绍', '核心概念'] }, { title: '主体', points: ['详细分析', '关键要素'] }, { title: '总结', points: ['核心结论', '建议方案'] } ] }; } catch { outline = { title: '生成文档', sections: [{ title: '主体', points: ['内容生成'] }] }; } // ★★★ 从 LLM 输出的 JSON 中提取 title ★★★ llmTitle = outline.title || ''; console.log('[MissionExecutor] LLM 生成标题(规划模式):', llmTitle); phases.planning.model = planningResult.modelId; phases.planning.attempts = planningResult.attempts; totalAttempts += planningResult.attempts; if (planningResult.ttfb < FAST_MODEL_THRESHOLD) { fastModelsUsed.push(planningResult.modelId); } } phases.planning.duration = Date.now() - planningStart; await this.updateProgress(taskId, 20, 'planning', '大纲生成完成'); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Phase 2: 分章扩写 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const sections = outline.sections || []; const expansionStart = Date.now(); const limit = pLimit(CONCURRENCY_LIMIT); const designTask = isDesignTask(params.taskType); console.log(`[MissionExecutor] Phase 2: 扩写 ${sections.length} 个章节${designTask ? ' (设计任务)' : ''}`); // ★★★ 设计任务:一次性完整生成(不拆章节) ★★★ let allSectionContent: string[] = []; if (designTask) { const designPrompt = params.style ? getStyledPrompt(params.taskType!, params.style) : getDesignPrompt(params.taskType!); await this.updateProgress(taskId, 30, 'writing', '设计任务:一次性生成完整内容...'); // ★★★ 在 user message 中强制重复输出格式要求 ★★★ const formatHint = params.taskType === 'pitch_deck' ? `【关键】你必须使用 标签输出,不要使用 标签。每个 slide 标签包含一个幻灯片。` : `【关键】你必须使用 标签输出。每个 file 标签包含一个文件。`; const designResult = await this.callNIMMissionCritical( taskId, 'expansion', [{ role: 'user', content: [ `## 任务需求`, ``, params.prompt, ``, formatHint, ``, `请严格按照 system prompt 中的输出格式要求,生成完整的内容。`, `所有内容必须包含在单个输出中,不要分多次输出。`, ].join('\n') }], 16000, designPrompt ); phases.expansion.attempts = designResult.attempts; totalAttempts += designResult.attempts; if (designResult.ttfb < FAST_MODEL_THRESHOLD) { fastModelsUsed.push(designResult.modelId); } allSectionContent = [designResult.content]; phases.expansion.sections = 1; await this.updateProgress(taskId, 80, 'writing', '设计内容生成完成'); } // ★★★ 非设计任务:分章节扩写 ★★★ const sectionTasks = designTask ? [] : sections.map((section, index) => limit(async () => { if (this.abortFlag) return ''; const startProgress = 20 + Math.floor(index / sections.length * 60); await this.updateProgress(taskId, startProgress, 'writing', `章节 ${index + 1}/${sections.length}: ${section.title}`); const isPaid = params.tier && params.tier !== 'free'; const expansionSystemPrompt = isPaid ? `你是腾讯/网易级资深系统策划,正在撰写「大师全案」。请深度扩写以下章节。 ═══════════════════════════════════════ 【反 AI 虚空废话协议 — 违反即不合格】 ═══════════════════════════════════════ 1. 禁止在线打草稿:不得出现「错误!应为」「修正:」「更正:」「实际上应该是」等自我纠错痕迹。一次成型,不回头。 2. 禁止学术名词复读:沉没成本/损失厌恶/认知失调/心理锚点等术语,全章最多提及1次(1句解释即止),此后用「该机制」「此设计」指代。 3. 禁止名人名言:不得引用彼得·德鲁克、乔布斯等人名言,不得使用「我们的使命是」「最危险的不是」等煽情句式。 4. 禁止 QA 自问自答:不得使用「你可能会问」「答案是」等伪对话。 5. 禁止模糊词:「很多」「大量」「显著」「一定」「若干」全部替换为具体数字或百分比。 6. 禁止泛化罗列:不得出现「首先...其次...最后...」八股文结构,不得用「概述」「简介」等虚词作章节标题。 ═══════════════════════════════════════ 【强制输出 — 涉及系统/功能/机制设计时必须包含】 ═══════════════════════════════════════ A. 数据结构表 — 每个核心系统必须给出字段定义: | 字段名 | 类型 | 说明 | 示例值 | B. 客户端-服务端流程 — 涉及网络请求时必须给出: 请求: POST /api/xxx → 请求体: {} → 响应: {} → 错误码: 列表 C. 边界情况表 — 每个系统必须列出 3-5 个异常场景: | 场景 | 触发条件 | 处理方式 | D. 核心循环 — 用箭头链描述主流程: 步骤A → 步骤B → [条件判断] → 步骤C ═══════════════════════════════════════ 输出格式标签(按场景选用,禁止使用未定义标签) ═══════════════════════════════════════ 1. 高管摘要(开篇必须使用,结论先行): 2-3句话概括本章最核心的结论或建议 2. 数据网格(核心数据/财务指标,最多4个): 指标 → 数值 | 指标 → 数值 | 指标 → 数值 3. KPI指标横排(每章最多1次): 指标 → 数值 | 指标 → 数值 4. 核心结论框(每章最多1次): 明确判断 + 数据依据 5. 操作步骤(执行类章节): 1. 步骤名 | 具体操作 + 预期结果 + 时间节点 6. 阶段进度: 第一阶段:xxx | 第二阶段:xxx | 第三阶段:xxx 7. 左右对比: 左侧内容 | 右侧内容 8. 话术演示: 对话脚本 9. 提示框: 具体建议 + 时间 + 责任角色 10. 警告框: 风险描述 + 规避方案 11. 检查清单: [x] 已完成 [ ] 待完成 12. 大数字突出: 13. 数据表格(Markdown): | 维度 | 现状 | 目标 | 差距 | ═══════════════════════════════════════ 内容质量标准 ═══════════════════════════════════════ ✓ 每章 800-1500 字,必须包含至少 1 个数据表格 + 1 个流程描述 ✓ 所有数字必须有单位和来源,禁止「显著提升」「一定效果」 ✓ 竞品案例必须具名(《原神》《王者荣耀》等),不用「某游戏」「某公司」 ✓ 执行建议必须有时间节点(「第1周」「上线后3天」)和责任主体 ✓ 涉及系统设计时,必须给出数据结构、接口定义、边界情况 ✓ 必须使用 3-5 种不同格式标签 风格:${params.style || '专业严谨,数据驱动,直击要害,像腾讯/网易内部策划案'}` : `你是资深系统策划,正在撰写「灵感草案」。帮用户快速看清核心问题和可行方向。 ═══════════════════════════════════════ 【反 AI 虚空废话协议 — 违反即不合格】 ═══════════════════════════════════════ 1. 禁止自我纠错:不得出现「错误」「修正」「更正」等痕迹。一次成型。 2. 禁止学术名词复读:术语全章最多1次,此后用「该机制」指代。 3. 禁止名人名言和煽情句式。 4. 禁止 QA 自问自答。 5. 禁止模糊词:用具体数字替代「很多」「大量」「显著」。 ═══════════════════════════════════════ 输出格式标签(精简版,按需选用) ═══════════════════════════════════════ 1. 高管摘要(开篇使用):1-2句核心结论 2. 核心数据:指标 → 数值 | 指标 → 数值 3. 核心结论(每章最多1个):关键判断 4. 注意事项:风险 + 规避方案 5. 行动建议:1. 步骤 | 说明 6. 对比框:左 | 右 7. 检查清单:[ ] 待完成 ═══════════════════════════════════════ 内容质量要求 ═══════════════════════════════════════ ✓ 每章 400-800 字,精炼不啰嗦 ✓ 每个要点 2-3 句话说清楚 ✓ 至少用 2 种格式标签 ✓ 涉及系统设计时给出核心数据结构和边界情况 ✓ 给方向的同时给出关键数字(转化率、成本、时间估算) 风格:${params.style || '简洁直接,专业务实,像资深同事给的建议'}`; // ★★★ 构建扩写用户消息:有草案时作为背景参考 ★★★ const draftReference = params.draftContent ? `\n## 背景参考(草案素材)\n\n⚠️ 以下内容仅作为背景参考和结构依据,不得照搬原文。你的任务是在此基础上重新创作一份专业全案,深度、数据、案例均需大幅扩充。\n\n${params.draftContent.slice(0, 500)}\n` : ''; const expansionResult = await this.callNIMMissionCritical( taskId, 'expansion', [{ role: 'user', content: [ `## 章节任务`, ``, `章节标题:**${section.title}**`, ``, `核心要点(需要深度覆盖):`, section.points.map((p, i) => `${i + 1}. ${p}`).join('\n'), ``, `原始用户需求(保持相关性):`, params.prompt.slice(0, 300), draftReference, `要求:`, isPaid ? `- 深度扩写,字数 800-1500字\n- 每个要点都要有数据支撑和具体案例\n- 必须使用至少 3 种格式标签\n- 禁止模糊表达,全部量化` : `- 精简扩写,字数 400-800字\n- 每个要点 2-3 句话,激发思路\n- 使用 2-3 种格式标签\n- 简洁直接`, ].join('\n') }], isPaid ? 4000 : 2000, expansionSystemPrompt ); const endProgress = 20 + Math.floor((index + 1) / sections.length * 60); await this.updateProgress(taskId, endProgress, 'writing', `章节 ${index + 1}/${sections.length}: ${section.title} (完成)`); phases.expansion.attempts += expansionResult.attempts; totalAttempts += expansionResult.attempts; if (expansionResult.ttfb < FAST_MODEL_THRESHOLD) { fastModelsUsed.push(expansionResult.modelId); } return expansionResult.content; }) ); const sectionResults = designTask ? allSectionContent : await Promise.all(sectionTasks); const validSections = sectionResults.filter(r => r.length > 0); phases.expansion.duration = Date.now() - expansionStart; phases.expansion.sections = validSections.length; phases.expansion.model = this.dispatcher.getNextModel() || 'multi'; if (!designTask) { await this.updateProgress(taskId, 80, 'writing', `生成 ${validSections.length} 个章节`); } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Phase 3: 质量自检(可选,设计任务跳过) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const fullContent = validSections.join('\n\n---\n\n'); const REVIEW_SKIP_THRESHOLD = 10000; // ★★★ 设计任务跳过 review(XML 标签会被 review 破坏)★★★ if (designTask) { console.log(`[MissionExecutor] ⚠️ 设计任务,跳过 Review`); phases.review.model = 'skipped-design-task'; phases.review.attempts = 0; } else if (fullContent.length > REVIEW_SKIP_THRESHOLD) { console.log(`[MissionExecutor] ⚠️ 内容过长(${fullContent.length}),跳过 Review`); phases.review.model = 'skipped'; phases.review.attempts = 0; } else { await this.updateProgress(taskId, 85, 'reviewing', '开始质量审查...'); const reviewStart = Date.now(); try { // ★★★ 修复:裁剪content到3000字符,maxTokens降到512 ★★★ // 模型maxContext=4096,需要给prompt留足够空间 const reviewContent = fullContent.length > 3000 ? fullContent.slice(0, 3000) + '\n...[内容已裁剪,仅审查前半部分]' : fullContent; const reviewResult = await this.callNIMMissionCritical( taskId, 'review', [{ role: 'user', content: reviewContent }], 1000, `你是腾讯/网易级文档质量审核编辑。执行以下审查: 【反 AI 废话审查 — 发现即删除/修正】 1. 删除所有自我纠错痕迹(「错误!应为」「修正:」「更正:」「实际上应该是」) 2. 删除名人名言和煽情愿景句式(「我们的使命是」「最危险的不是」) 3. 删除 QA 自问自答(「你可能会问」「答案是」) 4. 学术名词(沉没成本/损失厌恶/认知失调等)若出现超过1次,保留首次,删除后续重复 5. 模糊词(「很多」「大量」「显著」「一定」)替换为具体数字或删除 【内容质量审查】 6. 修正逻辑矛盾或前后不一致的表述 7. 补充遗漏的关键数据占位符(格式:[待补充:XXX数据]) 8. 优化过于口语化的表达为专业文档风格 【格式保护】 9. 严禁删除或修改 标签(exec_summary, metric_grid, kpi_row, highlight_box, info_box, step_box, phase_bar, comparison_box, script_box, warn_box, tip_box, checklist, number_card) 10. 严禁修改数据表格的 | 格式 11. 输出修正后的完整内容,不加任何说明文字` ); phases.review.model = reviewResult.modelId; phases.review.attempts = reviewResult.attempts; phases.review.duration = Date.now() - reviewStart; totalAttempts += reviewResult.attempts; // ★★★ Review结果仅替换审查部分 ★★★ const reviewedContent = reviewResult.content.length > reviewContent.length * 0.5 ? reviewResult.content + (fullContent.length > 3000 ? '\n\n' + fullContent.slice(3000) : '') : fullContent; await this.redis.set(`slow_task:content:${taskId}`, reviewedContent, 'EX', 86400 * 7); await this.updateProgress(taskId, 95, 'reviewing', '审查完成'); } catch (reviewErr) { console.warn(`[MissionExecutor] Review 失败,使用原文:`, reviewErr); phases.review.model = 'fallback'; phases.review.attempts = 0; await this.redis.set(`slow_task:content:${taskId}`, fullContent, 'EX', 86400 * 7); } } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 完成!写入最终状态 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const finalContent = await this.redis.get(`slow_task:content:${taskId}`) || fullContent; await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify({ taskId, status: 'completed', progress: 100, phase: 'completed', detail: `任务完成 (尝试 ${totalAttempts} 次,快速模型: ${fastModelsUsed.length})`, updatedAt: Date.now(), // downloadUrl 由 processor.ts 上传 COS 后设置(真实 URL) qualityGrade: totalAttempts <= 2 ? 'S' : totalAttempts <= 4 ? 'A' : 'B', sectionsGenerated: validSections.length, duration: Date.now() - startTime, phases, totalAttempts, fastModelsUsed }) }); console.log(`[MissionExecutor] ✅ 任务完成: ${taskId}, 总尝试: ${totalAttempts}, 快速模型: ${fastModelsUsed.join(',')}`); // ★★★ 设计任务附加输出元数据 ★★★ const designMeta = designTask ? getDesignTaskOutputMeta(params.taskType as DesignTaskType) : {}; return { content: typeof finalContent === 'string' ? finalContent : JSON.stringify(finalContent), outline, sectionsGenerated: validSections.length, duration: Date.now() - startTime, phases, totalAttempts, fastModelsUsed, title: llmTitle || outline.title || '', // ★ 优先使用 LLM 生成的标题 ...designMeta, }; } catch (err: any) { const safeMessage = sanitizeErrorMessage(err?.message); console.error(`[MissionExecutor] ❌ 任务失败: ${err.message}`); await this.updateProgress(taskId, 0, 'failed', safeMessage); throw err; } } /** * 更新进度 */ private async updateProgress(taskId: string, progress: number, phase: string, detail?: string): Promise { this.lastProgress = progress; this.lastPhase = phase; this.lastDetail = detail || ''; const status = progress === 100 ? 'completed' : progress === 0 ? 'failed' : 'writing'; // ★★★ 合并现有状态,避免覆盖 processor.ts 设置的 downloadUrl 等字段 ★★★ try { const existing = await this.redis.hget('slow_task:results', taskId); const prevState = existing ? (typeof existing === 'string' ? JSON.parse(existing) : existing) : {}; const report: ProgressReport = { ...prevState, taskId, status, progress, phase, detail: detail || '', updatedAt: Date.now() }; await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify(report) }); if (this.progressCallback) { this.progressCallback(report); } } catch { // fallback: 直接写入(不合并) const report: ProgressReport = { taskId, status, progress, phase, detail: detail || '', updatedAt: Date.now() }; await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify(report) }); if (this.progressCallback) { this.progressCallback(report); } } } /** * 注册进度回调 */ onProgress(callback: (report: ProgressReport) => void): void { this.progressCallback = callback; } /** * 中止任务 */ abort(): void { this.abortFlag = true; if (this.abortController) { this.abortController.abort(); console.warn('[MissionExecutor] 🛑 AbortController.abort() 已调用'); } } isAborted(): boolean { return this.abortFlag; } reset(): void { this.abortFlag = false; this.progressCallback = null; this.slowModelBlacklist.clear(); } /** * 执行 OpenGame 管线任务 */ private async executeOpenGame(taskId: string, params: TaskParams): Promise { const startTime = Date.now(); const outputDir = `/tmp/opengame/${taskId}`; try { const result = await runOpenGamePipeline({ prompt: params.prompt, style: typeof params.style === 'string' ? params.style : params.style?.name, complexity: 'medium', tier: params.tier as 'free' | 'pro' | 'enterprise' | undefined, outputDir, onProgress: (report) => { this.updateProgress(taskId, report.progress, report.stage, report.detail).catch(() => {}); }, }); const duration = Date.now() - startTime; const content = result.code.files.map(f => `// === ${f.path} ===\n${f.content}`).join('\n\n'); // 写入 Redis await this.redis.set(`slow_task:content:${taskId}`, content, 'EX', 86400 * 7); await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify({ taskId, status: 'completed', progress: 100, phase: 'completed', detail: `OpenGame 完成: ${result.filesWritten.length} 个文件, 验证 ${result.verification.passed ? '通过' : '未通过'}`, updatedAt: Date.now(), qualityGrade: result.verification.passed ? 'A' : 'B', }), }); return { content, outline: { title: result.gdd.metadata.title, sections: result.gdd.entities.map(e => ({ title: e.name, points: e.behaviors })), }, sectionsGenerated: result.code.files.length, duration, phases: { planning: { duration: 0, model: 'pipeline', attempts: 1 }, expansion: { duration: 0, model: 'pipeline', sections: result.code.files.length, attempts: 1 }, review: { duration: 0, model: 'pipeline', attempts: result.verification.rounds }, }, totalAttempts: result.verification.rounds, fastModelsUsed: ['opengame-pipeline'], outputType: 'zip', fileExtension: 'zip', contentType: 'application/zip', }; } catch (err: any) { const safeMessage = sanitizeErrorMessage(err?.message); await this.updateProgress(taskId, 0, 'failed', safeMessage); throw err; } } /** * 清洗 HTML 内容:移除 style/script/head 标签及其内容,再剥离剩余标签 */ private cleanHtmlContent(html: string): string { return html .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*<\/head>/gi, '') .replace(//g, '') .replace(/<[^>]*>/g, ' ') .replace(/&[a-z]+;/gi, ' ') .replace(/\s+/g, ' ') .trim(); } /** * 从草案提取大纲 */ private extractOutlineFromDraft(draft: string): TaskOutline { const sections: Array<{ title: string; points: string[] }> = []; // ★★★ 关键修复:先移除 style/script/head 标签内容,再提取标题 ★★★ const cleanedForStructure = draft .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*<\/head>/gi, '') .replace(//g, ''); const mdHeadingRegex = /^##\s+(.+)$/gm; let mdMatch; while ((mdMatch = mdHeadingRegex.exec(cleanedForStructure)) !== null) { sections.push({ title: mdMatch[1].trim(), points: ['延续草案风格'] }); } if (sections.length === 0) { const htmlHeadingRegex = /]*>([\s\S]*?)<\/h2>/gi; let htmlMatch; while ((htmlMatch = htmlHeadingRegex.exec(cleanedForStructure)) !== null) { sections.push({ title: htmlMatch[1].replace(/<[^>]*>/g, '').trim(), points: ['延续草案风格'] }); } } if (sections.length === 0) { sections.push({ title: '主体内容', points: ['详细阐述'] }); } const titleMatch = cleanedForStructure.match(/^#\s+(.+)$/m) || cleanedForStructure.match(/]*>([\s\S]*?)<\/h1>/i); const docTitle = titleMatch ? titleMatch[1].replace(/<[^>]*>/g, '').trim() : '策划方案'; return { title: docTitle, sections, metadata: { source: 'draft' } }; } } export function createTaskExecutor(dispatcher: ModelDispatcher, redis: Redis): TaskExecutor { return new TaskExecutor(dispatcher, redis); }