Procházet zdrojové kódy

feat: AI内容生成页面改为异步模式

- 后端新增异步生成接口 generate-async
- 后端新增任务状态查询接口 task/:taskId
- 后端 generateContentAsync 支持多阶段进度更新
- 前端改为异步轮询模式,避免长时间等待
- 前端创建任务后立即显示进度页面
- 前端每2秒轮询后端获取生成状态和结果
MyFramework User před 5 měsíci
rodič
revize
95397f46dd

+ 90 - 9
my-uniapp-vue3/src/pages/ai-content/index.vue

@@ -135,7 +135,7 @@
 
 <script setup lang="ts">
 import { ref, computed } from 'vue';
-import { post } from '../../utils/request';
+import { post, get } from '../../utils/request';
 
 const currentStep = ref(0);
 const prompt = ref('');
@@ -145,6 +145,8 @@ const targetLength = ref(1000);
 const generating = ref(false);
 const generatingProgress = ref('');
 const generatedContent = ref('');
+const currentTaskId = ref('');
+let pollTimer: ReturnType<typeof setInterval> | null = null;
 
 // UI状态
 const showHelp = ref(false);
@@ -163,6 +165,11 @@ const canGenerate = computed(() => {
 });
 
 function goBack() {
+  // 清理定时器
+  if (pollTimer) {
+    clearInterval(pollTimer);
+    pollTimer = null;
+  }
   uni.navigateBack();
 }
 
@@ -170,28 +177,99 @@ function useTemplate(tpl: { label: string; text: string }) {
   prompt.value = tpl.text;
 }
 
+/**
+ * 轮询任务状态
+ */
+async function pollTaskStatus(taskId: string) {
+  try {
+    const res = await get<any>(`/ai-content/task/${taskId}`);
+    const task = res;
+
+    // 更新进度显示
+    if (task.message) {
+      generatingProgress.value = task.message;
+    }
+
+    // 检查任务状态
+    if (task.status === 'completed') {
+      // 任务完成
+      stopPolling();
+      generatedContent.value = task.result?.content || task.result?.text || '生成内容为空';
+      generatingProgress.value = '生成完成';
+      generating.value = false;
+      uni.hideLoading();
+      currentStep.value = 1;  // 切换到结果显示
+      uni.showToast({ title: '生成完成', icon: 'success' });
+    } else if (task.status === 'failed') {
+      // 任务失败
+      stopPolling();
+      generating.value = false;
+      uni.hideLoading();
+      uni.showToast({ title: '生成失败: ' + (task.error || '未知错误'), icon: 'none' });
+    }
+    // pending 和 generating 状态继续轮询
+  } catch (e: any) {
+    console.error('轮询任务状态失败:', e);
+    // 轮询失败,继续尝试
+  }
+}
+
+/**
+ * 停止轮询
+ */
+function stopPolling() {
+  if (pollTimer) {
+    clearInterval(pollTimer);
+    pollTimer = null;
+  }
+}
+
 async function startGenerate() {
   if (!canGenerate.value) return;
 
   generating.value = true;
-  generatingProgress.value = 'AI正在分析需求并生成内容...';
+  generatingProgress.value = '正在创建生成任务...';
   generatedContent.value = '';
-  uni.showLoading({ title: '内容生成中...' });
+  currentStep.value = 1;  // 先切换到结果显示页面
+  uni.showLoading({ title: '创建任务...' });
 
   try {
-    const result = await post<any>('/ai-content/generate', {
+    // 1. 创建异步生成任务
+    // post 函数返回 result.data,所以 createRes 是 { taskId, status }
+    const createRes = await post<any>('/ai-content/generate-async', {
       prompt: prompt.value,
       targetLength: targetLength.value,
     });
 
-    generatedContent.value = result.content || result.text || '生成内容为空';
-    generatingProgress.value = '生成完成';
-    currentStep.value = 1;  // 切换到结果显示
+    // createRes 已经是 { taskId, status }
+    if (!createRes.taskId) {
+      throw new Error('创建任务失败:未获取到任务ID');
+    }
+
+    currentTaskId.value = createRes.taskId;
+    generatingProgress.value = '任务已创建,正在等待AI响应...';
+
+    // 2. 开始轮询任务状态
+    // 初始延迟,确保服务器有时间开始处理
+    await new Promise(resolve => setTimeout(resolve, 500));
+
+    // 立即查询一次
+    await pollTaskStatus(currentTaskId.value);
+
+    // 如果还没完成,开始定时轮询
+    if (generating.value) {
+      pollTimer = setInterval(() => {
+        if (currentTaskId.value && generating.value) {
+          pollTaskStatus(currentTaskId.value);
+        }
+      }, 2000);  // 每2秒轮询一次
+    }
+
   } catch (e: any) {
-    uni.showToast({ title: '生成失败: ' + (e.message || '未知错误'), icon: 'none' });
-  } finally {
     generating.value = false;
     uni.hideLoading();
+    currentStep.value = 0;  // 失败时回到输入页面
+    uni.showToast({ title: '创建任务失败: ' + (e.message || '未知错误'), icon: 'none' });
   }
 }
 
@@ -210,6 +288,9 @@ function useContent() {
 }
 
 function reset() {
+  // 清理定时器
+  stopPolling();
+  currentTaskId.value = '';
   currentStep.value = 0;
   generatedContent.value = '';
   generatingProgress.value = '';

+ 72 - 2
server/src/modules/ai-content/ai-content.controller.ts

@@ -3,7 +3,7 @@
  */
 
 import Router from '@koa/router';
-import { aiContentService } from './ai-content.service';
+import { aiContentService, generateTasks } from './ai-content.service';
 import { createLearningPath, getLearningPathDetail, getUserTasks, getPendingConfirmation, confirmAndContinue, regenerateNode, getAgentStatus } from './learning-path.service';
 
 const router = new Router();
@@ -179,7 +179,77 @@ router.get('/content-types/all', async (ctx) => {
   ctx.body = { code: 0, message: 'success', data: result };
 });
 
-// 统一内容生成接口
+// ============ 异步生成任务 API ============
+
+// 创建异步生成任务
+router.post('/generate-async', async (ctx) => {
+  const { prompt, targetLength } = ctx.request.body as {
+    prompt: string;
+    targetLength?: number;
+  };
+
+  if (!prompt || prompt.trim().length === 0) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '请输入内容描述' };
+    return;
+  }
+
+  const taskId = `gen-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+  const length = targetLength || 2000;
+
+  // 创建任务记录
+  generateTasks.set(taskId, {
+    id: taskId,
+    status: 'generating',
+    progress: 0,
+    message: '正在创建生成任务...',
+    createdAt: new Date(),
+  });
+
+  // 异步执行生成,不阻塞响应
+  aiContentService.generateContentAsync(taskId, prompt, length).catch(error => {
+    console.error('❌ 异步内容生成失败:', error);
+    const task = generateTasks.get(taskId);
+    if (task) {
+      task.status = 'failed';
+      task.error = error.message;
+      task.message = '生成失败: ' + error.message;
+    }
+  });
+
+  ctx.body = {
+    code: 0,
+    message: '任务已创建',
+    data: { taskId, status: 'generating' }
+  };
+});
+
+// 获取任务状态
+router.get('/task/:taskId', async (ctx) => {
+  const { taskId } = ctx.params;
+  const task = generateTasks.get(taskId);
+
+  if (!task) {
+    ctx.status = 404;
+    ctx.body = { code: 404, message: '任务不存在' };
+    return;
+  }
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: {
+      taskId: task.id,
+      status: task.status,
+      progress: task.progress,
+      message: task.message,
+      result: task.result,
+      error: task.error,
+    }
+  };
+});
+
+// 统一内容生成接口(同步版本,保留兼容)
 router.post('/generate', async (ctx) => {
   const { prompt, targetLength } = ctx.request.body as {
     prompt: string;

+ 79 - 0
server/src/modules/ai-content/ai-content.service.ts

@@ -6,6 +6,21 @@
 import axios from 'axios';
 import { config } from '../../config';
 
+// ============ 异步任务存储 ============
+
+export interface GenerateTask {
+  id: string;
+  status: 'pending' | 'generating' | 'completed' | 'failed';
+  progress: number;
+  message: string;
+  result?: any;
+  error?: string;
+  createdAt: Date;
+}
+
+// 存储生成任务
+export const generateTasks = new Map<string, GenerateTask>();
+
 // 内容类型分类
 const contentTypes = {
   '创作类': ['小说', '故事', '剧本', '诗歌', '散文'],
@@ -1032,6 +1047,70 @@ ${text}
       selected: 0,
     };
   }
+
+  /**
+   * 异步内容生成(支持进度更新)
+   */
+  async generateContentAsync(taskId: string, prompt: string, targetLength: number = 2000) {
+    const updateTask = (updates: Partial<{ status: string; progress: number; message: string; result: any }>) => {
+      const task = generateTasks.get(taskId);
+      if (task) {
+        Object.assign(task, updates);
+      }
+    };
+
+    try {
+      // 阶段1:分析需求
+      updateTask({ progress: 10, message: '正在分析需求...' });
+      await new Promise(resolve => setTimeout(resolve, 500));
+
+      // 阶段2:构建Prompt
+      updateTask({ progress: 20, message: '正在构建生成Prompt...' });
+      await new Promise(resolve => setTimeout(resolve, 300));
+
+      // 系统性知识展开
+      const finalPrompt = `你是一位专业的老师。请围绕用户的主题,系统性地讲解这个知识点。
+
+用户主题:${prompt}
+
+要求:
+1. 首先分析这个主题涉及的核心领域和知识体系
+2. 按照"大类 -> 小类 -> 具体知识点"的层次结构展开讲解
+3. 每个知识点都要讲清楚"是什么"、"为什么"、"怎么用"
+4. 内容要准确、全面、深入浅出
+5. 直接返回正文内容,用清晰的章节标题组织结构
+6. 目标字数:${targetLength}字左右,如果内容有价值可以超出`;
+
+      console.log('🤖 [AI异步内容生成] 使用模型:', this.model);
+
+      // 阶段3:调用AI
+      updateTask({ progress: 30, message: '正在调用AI生成内容...' });
+
+      const content = await this.callLLM(finalPrompt);
+
+      // 阶段4:整理结果
+      updateTask({ progress: 80, message: '正在整理生成结果...' });
+      await new Promise(resolve => setTimeout(resolve, 200));
+
+      // 阶段5:完成
+      updateTask({ progress: 100, message: '生成完成!', status: 'completed' });
+
+      const result = {
+        content,
+        type: '通用',
+        industry: '通用',
+        wordCount: content.length,
+      };
+
+      updateTask({ result });
+
+      return result;
+    } catch (error: any) {
+      console.error('❌ 异步内容生成失败:', error);
+      updateTask({ status: 'failed', message: '生成失败: ' + error.message });
+      throw error;
+    }
+  }
 }
 
 export const aiContentService = new AIContentService();