瀏覽代碼

feat: 学习路径多Agent模式和TTS全面优化

新增功能:
- 学习路径多Agent协作模式(渐进确认+多Agent并行)
- TTS异步生成(立即返回audioId,前端轮询状态)
- 随机模型选择(qwen3-tts-flash)
- API重试机制(429/500错误指数退避)

修复问题:
- GET /ai/models 改用GET
- 音频合并WAV->MP3转码
- TTS分段限制480字符
- 禁用不兼容模型(vd/vc等403)

包含页面:
- learning-path(学习路径)
- ai-content(AI内容生成)
- AI对话页面优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MyFramework User 5 月之前
父節點
當前提交
5122e0273a

+ 6 - 0
my-uniapp-vue3/src/pages.json

@@ -55,6 +55,12 @@
         "navigationStyle": "custom"
         "navigationStyle": "custom"
       }
       }
     },
     },
+    {
+      "path": "pages/learning-path/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
     {
     {
       "path": "pages/favorites/index",
       "path": "pages/favorites/index",
       "style": {
       "style": {

+ 80 - 354
my-uniapp-vue3/src/pages/ai-content/index.vue

@@ -15,59 +15,37 @@
 
 
     <!-- 主内容区 -->
     <!-- 主内容区 -->
     <view class="main-content">
     <view class="main-content">
-      <!-- 步骤1:基础设置 -->
+      <!-- 步骤1:输入需求 -->
       <view v-if="currentStep === 0" class="card">
       <view v-if="currentStep === 0" class="card">
         <view class="card-header">
         <view class="card-header">
-          <text class="card-title">📋 内容设置</text>
+          <text class="card-title">📝 描述你的需求</text>
         </view>
         </view>
 
 
-        <!-- 内容类型 -->
+        <!-- 需求输入 -->
         <view class="form-item">
         <view class="form-item">
-          <text class="form-label">内容类型</text>
-          <scroll-view scroll-x class="type-scroll">
-            <view class="type-list">
-              <view
-                v-for="type in allTypes"
-                :key="type"
-                class="type-item"
-                :class="{ active: selectedType === type }"
-                @click="selectedType = type"
-              >
-                {{ type }}
-              </view>
-            </view>
-          </scroll-view>
-        </view>
-
-        <!-- 行业选择 -->
-        <view class="form-item">
-          <text class="form-label">所属行业</text>
-          <scroll-view scroll-x class="industry-scroll">
-            <view class="industry-list">
-              <view
-                v-for="ind in industries"
-                :key="ind"
-                class="industry-item"
-                :class="{ active: selectedIndustry === ind }"
-                @click="selectedIndustry = ind"
-              >
-                {{ ind }}
-              </view>
-            </view>
-          </scroll-view>
-        </view>
-
-        <!-- 主题/需求输入 -->
-        <view class="form-item">
-          <text class="form-label">创作需求</text>
           <textarea
           <textarea
             v-model="prompt"
             v-model="prompt"
             class="prompt-input"
             class="prompt-input"
-            :placeholder="getPromptPlaceholder()"
-            :maxlength="500"
+            placeholder="例如:帮我写一篇关于人工智能的科普文章,适合在公众号发布"
+            :maxlength="1000"
           />
           />
           <view class="input-hint">
           <view class="input-hint">
-            <text>{{ prompt.length }}/500 字</text>
+            <text>{{ prompt.length }}/1000 字</text>
+          </view>
+        </view>
+
+        <!-- 快速模板 -->
+        <view class="quick-templates">
+          <text class="template-label">快速模板:</text>
+          <view class="template-list">
+            <view
+              v-for="tpl in quickTemplates"
+              :key="tpl.text"
+              class="template-item"
+              @click="useTemplate(tpl)"
+            >
+              {{ tpl.label }}
+            </view>
           </view>
           </view>
         </view>
         </view>
 
 
@@ -77,7 +55,7 @@
           <slider
           <slider
             :value="targetLength"
             :value="targetLength"
             :min="100"
             :min="100"
-            :max="10000"
+            :max="5000"
             :step="100"
             :step="100"
             @change="(e: any) => targetLength = e.detail.value"
             @change="(e: any) => targetLength = e.detail.value"
             activeColor="#4F46E5"
             activeColor="#4F46E5"
@@ -86,7 +64,7 @@
           />
           />
           <view class="slider-range">
           <view class="slider-range">
             <text>100字</text>
             <text>100字</text>
-            <text>10000字</text>
+            <text>5000字</text>
           </view>
           </view>
         </view>
         </view>
 
 
@@ -95,7 +73,7 @@
           :disabled="!canGenerate || generating"
           :disabled="!canGenerate || generating"
           @click="startGenerate"
           @click="startGenerate"
         >
         >
-          <text v-if="generating">{{ generatingProgress || '生成中...' }}</text>
+          <text v-if="generating">{{ generatingProgress }}</text>
           <text v-else>🚀 开始生成</text>
           <text v-else>🚀 开始生成</text>
         </button>
         </button>
       </view>
       </view>
@@ -106,7 +84,6 @@
           <text class="card-title">📝 生成结果</text>
           <text class="card-title">📝 生成结果</text>
           <view class="header-actions">
           <view class="header-actions">
             <text class="header-action" @click="copyContent">📋 复制</text>
             <text class="header-action" @click="copyContent">📋 复制</text>
-            <text class="header-action" @click="saveDraft">💾 保存</text>
           </view>
           </view>
         </view>
         </view>
 
 
@@ -114,14 +91,7 @@
         <view v-if="generating" class="progress-section">
         <view v-if="generating" class="progress-section">
           <view class="progress-header">
           <view class="progress-header">
             <text>{{ generatingProgress }}</text>
             <text>{{ generatingProgress }}</text>
-            <text v-if="currentCharCount > 0">{{ currentCharCount }} 字</text>
           </view>
           </view>
-          <progress
-            :percent="Math.round(progressPercent)"
-            activeColor="#4F46E5"
-            backgroundColor="#e5e7eb"
-            stroke-width="8"
-          />
         </view>
         </view>
 
 
         <!-- 生成的内容 -->
         <!-- 生成的内容 -->
@@ -138,69 +108,6 @@
           <button class="primary-btn" @click="useContent">📝 使用此内容</button>
           <button class="primary-btn" @click="useContent">📝 使用此内容</button>
         </view>
         </view>
       </view>
       </view>
-
-      <!-- 步骤3:质量检测 -->
-      <view v-if="currentStep === 2" class="card">
-        <view class="card-header">
-          <text class="card-title">✅ 质量检测</text>
-        </view>
-
-        <!-- 敏感词检测 -->
-        <view class="quality-item">
-          <view class="quality-header">
-            <text class="quality-title">🔍 敏感词检测</text>
-            <text class="quality-status" :class="sensitiveResult.isClean ? 'pass' : 'fail'">
-              {{ sensitiveResult.isClean ? '通过' : '有问题' }}
-            </text>
-          </view>
-          <view v-if="sensitiveResult.foundWords.length > 0" class="sensitive-words">
-            <text v-for="word in sensitiveResult.foundWords" :key="word" class="sensitive-tag">
-              {{ word }}
-            </text>
-          </view>
-        </view>
-
-        <!-- 合规检查 -->
-        <view class="quality-item">
-          <view class="quality-header">
-            <text class="quality-title">📋 合规检查</text>
-            <text class="quality-status" :class="complianceResult.passed ? 'pass' : 'fail'">
-              {{ complianceResult.passed ? '通过' : '有问题' }}
-            </text>
-          </view>
-          <view v-if="complianceResult.warnings.length > 0" class="compliance-warnings">
-            <text v-for="warn in complianceResult.warnings" :key="warn" class="warning-tag">
-              {{ warn }}
-            </text>
-          </view>
-        </view>
-
-        <!-- 质量评分 -->
-        <view v-if="qualityScore" class="quality-item">
-          <view class="quality-header">
-            <text class="quality-title">📊 内容评分</text>
-            <text class="quality-score">{{ qualityScore.overall }}分</text>
-          </view>
-          <view class="score-dimensions">
-            <view
-              v-for="(value, key) in qualityScore.dimensions"
-              :key="key"
-              class="dimension-item"
-            >
-              <text class="dimension-name">{{ getDimensionName(key) }}</text>
-              <view class="dimension-bar">
-                <view class="dimension-fill" :style="{ width: value + '%' }"></view>
-              </view>
-              <text class="dimension-value">{{ Math.round(value) }}</text>
-            </view>
-          </view>
-        </view>
-
-        <view class="btn-row">
-          <button class="back-btn" @click="currentStep = 1">← 返回结果</button>
-          <button class="primary-btn" @click="useContent">📝 使用此内容</button>
-        </view>
-      </view>
     </view>
     </view>
 
 
     <!-- 帮助弹窗 -->
     <!-- 帮助弹窗 -->
@@ -211,17 +118,15 @@
           <text class="modal-close" @click="showHelp = false">✕</text>
           <text class="modal-close" @click="showHelp = false">✕</text>
         </view>
         </view>
         <view class="modal-body">
         <view class="modal-body">
+          <text class="help-text">1. 描述你想要生成的内容</text>
+          <text class="help-text">2. 系统会自动识别内容类型</text>
+          <text class="help-text">3. 点击生成即可获得内容</text>
           <text class="help-title">支持的类型:</text>
           <text class="help-title">支持的类型:</text>
-          <text class="help-text">• 创作类:小说、故事、剧本、诗歌、散文</text>
-          <text class="help-text">• 营销类:产品介绍、广告文案、朋友圈、小红书、抖音脚本</text>
-          <text class="help-text">• 教育类:课件、培训、教程、知识科普</text>
-          <text class="help-text">• 商务类:销售话术、客服话术、商务邮件</text>
-          <text class="help-text">• 媒体类:新闻播报、天气预报、体育解说</text>
-          <text class="help-title">使用流程:</text>
-          <text class="help-text">1. 选择内容类型</text>
-          <text class="help-text">2. 选择所属行业</text>
-          <text class="help-text">3. 描述您的需求</text>
-          <text class="help-text">4. 一键生成</text>
+          <text class="help-text">• 小说、故事、剧本</text>
+          <text class="help-text">• 营销文案(朋友圈、小红书、抖音)</text>
+          <text class="help-text">• 培训课件、销售话术</text>
+          <text class="help-text">• 新闻播报、天气预报</text>
+          <text class="help-text">• 各种日常工作文案</text>
         </view>
         </view>
       </view>
       </view>
     </view>
     </view>
@@ -232,129 +137,64 @@
 import { ref, computed } from 'vue';
 import { ref, computed } from 'vue';
 import { post } from '../../utils/request';
 import { post } from '../../utils/request';
 
 
-const allTypes = [
-  '小说', '故事', '剧本', '诗歌', '散文',
-  '产品介绍', '广告文案', '朋友圈', '小红书', '抖音脚本',
-  '课件', '培训', '教程', '知识科普',
-  '销售话术', '客服话术', '商务邮件',
-  '新闻播报', '天气预报', '体育解说',
-];
-
-const industries = ['通用', '医疗健康', '教育培训', '金融服务', '电子商务', '餐饮美食'];
-
 const currentStep = ref(0);
 const currentStep = ref(0);
-const selectedType = ref('小说');
-const selectedIndustry = ref('通用');
 const prompt = ref('');
 const prompt = ref('');
-const targetLength = ref(2000);
+const targetLength = ref(1000);
 
 
 // 生成状态
 // 生成状态
 const generating = ref(false);
 const generating = ref(false);
 const generatingProgress = ref('');
 const generatingProgress = ref('');
-const currentCharCount = ref(0);
 const generatedContent = ref('');
 const generatedContent = ref('');
 
 
-// 质量检测
-const sensitiveResult = ref<{ isClean: boolean; foundWords: string[] }>({ isClean: true, foundWords: [] });
-const complianceResult = ref<{ passed: boolean; warnings: string[] }>({ passed: true, warnings: [] });
-const qualityScore = ref<any>(null);
-
 // UI状态
 // UI状态
 const showHelp = ref(false);
 const showHelp = ref(false);
 
 
+const quickTemplates = [
+  { label: '朋友圈', text: '发一条朋友圈,分享今天的心情' },
+  { label: '小红书', text: '写一篇小红书种草文案,推荐一款好用的护肤品' },
+  { label: '产品文案', text: '为我们的智能手表写一篇产品介绍,突出心率监测和防水功能' },
+  { label: '培训课件', text: '制作一份新员工培训课件,主题是公司文化和工作流程' },
+  { label: '销售话术', text: '写一套电话销售话术,针对有意向的客户' },
+  { label: '故事', text: '写一个温馨感人的亲情故事' },
+];
+
 const canGenerate = computed(() => {
 const canGenerate = computed(() => {
   return prompt.value.trim().length > 0 && !generating.value;
   return prompt.value.trim().length > 0 && !generating.value;
 });
 });
 
 
-const progressPercent = computed(() => {
-  if (!generating.value) return 100;
-  return 50; // 生成中固定50%
-});
-
-const dimensionNames: Record<string, string> = {
-  fluency: '流畅度',
-  naturalness: '自然度',
-  emotion_consistency: '情感一致',
-  topic_adherence: '主题相关',
-  structural_integrity: '结构完整',
-};
-
-function getDimensionName(key: string) {
-  return dimensionNames[key] || key;
-}
-
-function getPromptPlaceholder() {
-  const placeholders: Record<string, string> = {
-    '小说': '描述你想要的故事类型,如:都市爱情、玄幻穿越、悬疑推理...',
-    '故事': '描述故事的主题和背景...',
-    '产品介绍': '介绍产品的名称、特点、优势...',
-    '广告文案': '描述产品卖点、活动内容...',
-    '朋友圈': '描述想表达的心情或内容...',
-    '培训': '描述培训主题和目标受众...',
-    '销售话术': '描述产品类型和目标客户...',
-    '新闻播报': '描述新闻事件内容...',
-  };
-  return placeholders[selectedType.value] || '描述您的创作需求...';
-}
-
 function goBack() {
 function goBack() {
   uni.navigateBack();
   uni.navigateBack();
 }
 }
 
 
+function useTemplate(tpl: { label: string; text: string }) {
+  prompt.value = tpl.text;
+}
+
 async function startGenerate() {
 async function startGenerate() {
   if (!canGenerate.value) return;
   if (!canGenerate.value) return;
 
 
   generating.value = true;
   generating.value = true;
-  generatingProgress.value = '正在生成内容...';
-  currentCharCount.value = 0;
+  generatingProgress.value = 'AI正在分析需求并生成内容...';
   generatedContent.value = '';
   generatedContent.value = '';
+  uni.showLoading({ title: '内容生成中...' });
 
 
   try {
   try {
     const result = await post<any>('/ai-content/generate', {
     const result = await post<any>('/ai-content/generate', {
-      type: selectedType.value,
-      industry: selectedIndustry.value,
       prompt: prompt.value,
       prompt: prompt.value,
       targetLength: targetLength.value,
       targetLength: targetLength.value,
     });
     });
 
 
     generatedContent.value = result.content || result.text || '生成内容为空';
     generatedContent.value = result.content || result.text || '生成内容为空';
-    currentCharCount.value = generatedContent.value.length;
     generatingProgress.value = '生成完成';
     generatingProgress.value = '生成完成';
-
-    // 自动进行质量检测
-    await checkQuality();
-    currentStep.value = 1;
+    currentStep.value = 1;  // 切换到结果显示
   } catch (e: any) {
   } catch (e: any) {
     uni.showToast({ title: '生成失败: ' + (e.message || '未知错误'), icon: 'none' });
     uni.showToast({ title: '生成失败: ' + (e.message || '未知错误'), icon: 'none' });
   } finally {
   } finally {
     generating.value = false;
     generating.value = false;
+    uni.hideLoading();
   }
   }
 }
 }
 
 
-async function checkQuality() {
-  if (!generatedContent.value) return;
-
-  // 敏感词检测
-  try {
-    const sensitive = await post<any>('/ai-content/sensitive/check', { text: generatedContent.value });
-    sensitiveResult.value = sensitive;
-  } catch (e) {}
-
-  // 合规检查
-  try {
-    const compliance = await post<any>('/ai-content/compliance/check', {
-      text: generatedContent.value,
-      industry: selectedIndustry.value,
-    });
-    complianceResult.value = compliance;
-  } catch (e) {}
-
-  // 质量评分
-  try {
-    qualityScore.value = await post<any>('/ai-content/quality/score', { text: generatedContent.value });
-  } catch (e) {}
-}
-
 function copyContent() {
 function copyContent() {
   uni.setClipboardData({
   uni.setClipboardData({
     data: generatedContent.value,
     data: generatedContent.value,
@@ -364,10 +204,6 @@ function copyContent() {
   });
   });
 }
 }
 
 
-function saveDraft() {
-  uni.showToast({ title: '已保存草稿', icon: 'success' });
-}
-
 function useContent() {
 function useContent() {
   uni.setStorageSync('ai_generated_text', generatedContent.value);
   uni.setStorageSync('ai_generated_text', generatedContent.value);
   uni.navigateBack();
   uni.navigateBack();
@@ -377,11 +213,6 @@ function reset() {
   currentStep.value = 0;
   currentStep.value = 0;
   generatedContent.value = '';
   generatedContent.value = '';
   generatingProgress.value = '';
   generatingProgress.value = '';
-  currentCharCount.value = 0;
-}
-
-function showQualityScore() {
-  currentStep.value = 2;
 }
 }
 </script>
 </script>
 
 
@@ -469,44 +300,10 @@ function showQualityScore() {
   margin-bottom: 32rpx;
   margin-bottom: 32rpx;
 }
 }
 
 
-.form-label {
-  font-size: 28rpx;
-  color: #6b7280;
-  margin-bottom: 16rpx;
-  display: block;
-}
-
-/* 类型选择 */
-.type-scroll, .industry-scroll {
-  white-space: nowrap;
-}
-
-.type-list, .industry-list {
-  display: inline-flex;
-  gap: 16rpx;
-  padding: 8rpx 0;
-}
-
-.type-item, .industry-item {
-  font-size: 26rpx;
-  padding: 12rpx 24rpx;
-  background: #f3f4f6;
-  border-radius: 32rpx;
-  color: #6b7280;
-  white-space: nowrap;
-  border: 2rpx solid transparent;
-}
-
-.type-item.active, .industry-item.active {
-  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
-  color: #fff;
-  border-color: #4f46e5;
-}
-
 /* 输入框 */
 /* 输入框 */
 .prompt-input {
 .prompt-input {
   width: 100%;
   width: 100%;
-  min-height: 200rpx;
+  min-height: 250rpx;
   background: #f9fafb;
   background: #f9fafb;
   border-radius: 16rpx;
   border-radius: 16rpx;
   padding: 24rpx;
   padding: 24rpx;
@@ -524,6 +321,32 @@ function showQualityScore() {
   color: #9ca3af;
   color: #9ca3af;
 }
 }
 
 
+/* 快速模板 */
+.quick-templates {
+  margin-bottom: 32rpx;
+}
+
+.template-label {
+  font-size: 26rpx;
+  color: #6b7280;
+  margin-bottom: 16rpx;
+  display: block;
+}
+
+.template-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+
+.template-item {
+  font-size: 24rpx;
+  padding: 10rpx 20rpx;
+  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
+  color: #fff;
+  border-radius: 24rpx;
+}
+
 /* 滑块 */
 /* 滑块 */
 .slider-range {
 .slider-range {
   display: flex;
   display: flex;
@@ -546,13 +369,12 @@ function showQualityScore() {
 
 
 .progress-header {
 .progress-header {
   display: flex;
   display: flex;
-  justify-content: space-between;
-  margin-bottom: 12rpx;
+  justify-content: center;
 }
 }
 
 
 .progress-header text {
 .progress-header text {
-  font-size: 26rpx;
-  color: #6b7280;
+  font-size: 28rpx;
+  color: #4F46E5;
 }
 }
 
 
 /* 结果 */
 /* 结果 */
@@ -581,102 +403,6 @@ function showQualityScore() {
   color: #9ca3af;
   color: #9ca3af;
 }
 }
 
 
-/* 质量检测 */
-.quality-item {
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  margin-bottom: 16rpx;
-}
-
-.quality-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12rpx;
-}
-
-.quality-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-.quality-status {
-  font-size: 24rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 8rpx;
-}
-
-.quality-status.pass {
-  background: #d1fae5;
-  color: #059669;
-}
-
-.quality-status.fail {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-.quality-score {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #4F46E5;
-}
-
-.sensitive-words, .compliance-warnings {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 12rpx;
-}
-
-.sensitive-tag, .warning-tag {
-  font-size: 22rpx;
-  padding: 4rpx 12rpx;
-  background: #fee2e2;
-  color: #dc2626;
-  border-radius: 6rpx;
-}
-
-.score-dimensions {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-}
-
-.dimension-item {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-}
-
-.dimension-name {
-  font-size: 24rpx;
-  color: #6b7280;
-  width: 120rpx;
-}
-
-.dimension-bar {
-  flex: 1;
-  height: 12rpx;
-  background: #e5e7eb;
-  border-radius: 6rpx;
-  overflow: hidden;
-}
-
-.dimension-fill {
-  height: 100%;
-  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
-  border-radius: 6rpx;
-}
-
-.dimension-value {
-  font-size: 24rpx;
-  color: #4F46E5;
-  width: 50rpx;
-  text-align: right;
-}
-
 /* 按钮 */
 /* 按钮 */
 .btn-row {
 .btn-row {
   display: flex;
   display: flex;

+ 2 - 2
my-uniapp-vue3/src/pages/index/index.vue

@@ -179,8 +179,8 @@ onShow(() => {
 <style scoped>
 <style scoped>
 .page {
 .page {
   min-height: 100vh;
   min-height: 100vh;
-  background: #f5f5f5;
-  padding-bottom: 180rpx;
+  background: #f9fafb;
+  padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
 }
 }
 
 
 .search-bar {
 .search-bar {

+ 1042 - 0
my-uniapp-vue3/src/pages/learning-path/index.vue

@@ -0,0 +1,1042 @@
+<template>
+  <view class="page">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-content">
+        <view class="nav-left" @click="goBack">
+          <text class="back-icon">←</text>
+        </view>
+        <text class="page-title">学习路径</text>
+        <view class="nav-right">
+          <text></text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 主内容区 -->
+    <view class="main-content">
+      <!-- 创建新任务 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">🎯 创建学习路径</text>
+        </view>
+        <textarea
+          v-model="newTopic"
+          class="topic-input"
+          placeholder="例如:我想学计算机 / 我想了解量子物理 / 我想学做川菜"
+          :maxlength="500"
+        />
+
+        <!-- 模式选择 -->
+        <view class="mode-selector">
+          <view class="mode-option" :class="{ active: selectedMode === 'progressive' }" @click="selectedMode = 'progressive'">
+            <text class="mode-icon">🔍</text>
+            <view class="mode-info">
+              <text class="mode-name">渐进模式</text>
+              <text class="mode-desc">逐步确认,灵活调整</text>
+            </view>
+          </view>
+          <view class="mode-option" :class="{ active: selectedMode === 'multi-agent' }" @click="selectedMode = 'multi-agent'">
+            <text class="mode-icon">🚀</text>
+            <view class="mode-info">
+              <text class="mode-name">多Agent模式</text>
+              <text class="mode-desc">全自动并行生成</text>
+            </view>
+          </view>
+        </view>
+
+        <button
+          class="create-btn"
+          :disabled="!newTopic.trim() || creating"
+          @click="createTask"
+        >
+          <text v-if="creating">创建中...</text>
+          <text v-else>🚀 开始生成</text>
+        </button>
+      </view>
+
+      <!-- 任务列表 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">📚 我的任务</text>
+          <text class="refresh-btn" @click="loadTasks">🔄</text>
+        </view>
+        <view v-if="loadingTasks" class="loading">
+          <text>加载中...</text>
+        </view>
+        <view v-else-if="tasks.length === 0" class="empty">
+          <text>暂无任务</text>
+        </view>
+        <view v-else class="task-list">
+          <view
+            v-for="task in tasks"
+            :key="task.id"
+            class="task-item"
+            @click="viewTask(task.id)"
+          >
+            <view class="task-info">
+              <view class="task-left">
+                <text class="task-topic">{{ task.topic }}</text>
+                <view class="task-meta">
+                  <text class="task-mode" :class="task.generationMode">{{ task.generationMode === 'multi-agent' ? '🚀多Agent' : '🔍渐进' }}</text>
+                  <text class="task-status" :class="task.status">
+                    {{ statusText[task.status] || task.status }}
+                  </text>
+                </view>
+              </view>
+            </view>
+            <view class="task-progress">
+              <view class="progress-bar">
+                <view class="progress-fill" :style="{ width: task.progress + '%' }"></view>
+              </view>
+              <text class="progress-text">{{ task.progress }}%</text>
+            </view>
+            <text class="task-date">{{ formatDate(task.createdAt) }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 任务详情 -->
+      <view v-if="currentTask" class="card task-detail">
+        <view class="card-header">
+          <text class="card-title">📖 {{ currentTask.topic }}</text>
+          <text class="close-btn" @click="closeDetail">×</text>
+        </view>
+
+        <!-- 总体进度 -->
+        <view class="overall-progress">
+          <view class="progress-info">
+            <text>总体进度: {{ currentTask.progress }}%</text>
+            <text>{{ currentTask.completedCount }}/{{ currentTask.totalCount }} 内容块</text>
+          </view>
+          <view class="progress-bar large">
+            <view class="progress-fill" :style="{ width: currentTask.progress + '%' }"></view>
+          </view>
+        </view>
+
+        <!-- 多Agent模式状态 -->
+        <view v-if="currentTask.generationMode === 'multi-agent'" class="agent-status">
+          <text class="agent-title">🚀 Agent 状态</text>
+          <view v-if="agentStatus" class="agent-list">
+            <view v-for="agent in agentStatus.agents" :key="agent.name" class="agent-item">
+              <text class="agent-name">{{ agent.name }}</text>
+              <view class="agent-progress">
+                <view class="progress-bar small">
+                  <view class="progress-fill" :style="{ width: agent.progress + '%' }"></view>
+                </view>
+                <text class="agent-percent">{{ agent.progress }}%</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 渐进模式确认面板 -->
+        <view v-if="currentTask.generationMode === 'progressive' && pendingData" class="confirm-panel">
+          <text class="confirm-title">📋 待确认内容</text>
+
+          <!-- 学科确认 -->
+          <view v-if="pendingData.currentStep === 'subjects'" class="confirm-section">
+            <text class="confirm-step">第1步: 确认学科列表</text>
+            <view class="confirm-list">
+              <view
+                v-for="subject in pendingData.pendingSubjects"
+                :key="subject.id"
+                class="confirm-item"
+              >
+                <text class="item-icon">📚</text>
+                <text class="item-name">{{ subject.name }}</text>
+              </view>
+            </view>
+            <button class="confirm-btn" @click="confirmStep('subjects')" :disabled="confirming">
+              <text v-if="confirming">确认中...</text>
+              <text v-else>✅ 确认学科并继续</text>
+            </button>
+          </view>
+
+          <!-- 章节确认 -->
+          <view v-if="pendingData.currentStep === 'chapters'" class="confirm-section">
+            <text class="confirm-step">第2步: 确认章节列表</text>
+            <view v-for="(chapters, subjectId) in pendingData.pendingChapters" :key="subjectId" class="subject-chapters">
+              <text class="subject-label">📚 {{ getSubjectName(subjectId) }}</text>
+              <view class="confirm-list">
+                <view v-for="chapter in chapters" :key="chapter.id" class="confirm-item">
+                  <text class="item-icon">📖</text>
+                  <text class="item-name">{{ chapter.name }}</text>
+                </view>
+              </view>
+            </view>
+            <button class="confirm-btn" @click="confirmStep('chapters')" :disabled="confirming">
+              <text v-if="confirming">确认中...</text>
+              <text v-else>✅ 确认章节并继续</text>
+            </button>
+          </view>
+
+          <!-- 小节确认 -->
+          <view v-if="pendingData.currentStep === 'sections'" class="confirm-section">
+            <text class="confirm-step">第3步: 确认小节列表</text>
+            <view v-for="(sections, chapterId) in pendingData.pendingSections" :key="chapterId" class="chapter-sections">
+              <text class="chapter-label">📖 {{ getChapterName(chapterId) }}</text>
+              <view class="confirm-list">
+                <view v-for="section in sections" :key="section.id" class="confirm-item">
+                  <text class="item-icon">📑</text>
+                  <text class="item-name">{{ section.name }}</text>
+                </view>
+              </view>
+            </view>
+            <button class="confirm-btn" @click="confirmStep('sections')" :disabled="confirming">
+              <text v-if="confirming">确认中...</text>
+              <text v-else>✅ 确认小节并生成内容</text>
+            </button>
+          </view>
+
+          <!-- 生成内容中 -->
+          <view v-if="pendingData.currentStep === 'content'" class="confirm-section">
+            <text class="confirm-step">⏳ 正在生成详细内容...</text>
+            <text class="confirm-hint">请稍候,系统正在为每个小节生成详细内容</text>
+          </view>
+        </view>
+
+        <!-- 学科列表 -->
+        <view v-if="currentTask.subjects && currentTask.subjects.length > 0" class="subjects">
+          <view
+            v-for="subject in currentTask.subjects"
+            :key="subject.id"
+            class="subject-item"
+          >
+            <view class="subject-header" @click="toggleSubject(subject)">
+              <text class="subject-icon">{{ subject.expanded ? '📂' : '📁' }}</text>
+              <text class="subject-name">{{ subject.name }}</text>
+              <text class="subject-status" :class="subject.status">{{ statusText[subject.status] }}</text>
+            </view>
+
+            <!-- 章节列表 -->
+            <view v-if="subject.expanded && subject.chapters" class="chapters">
+              <view
+                v-for="chapter in subject.chapters"
+                :key="chapter.id"
+                class="chapter-item"
+              >
+                <view class="chapter-header" @click="toggleChapter(chapter)">
+                  <text class="chapter-icon">{{ chapter.expanded ? '📗' : '📄' }}</text>
+                  <text class="chapter-name">{{ chapter.name }}</text>
+                </view>
+
+                <!-- 小节列表 -->
+                <view v-if="chapter.expanded && chapter.sections" class="sections">
+                  <view
+                    v-for="section in chapter.sections"
+                    :key="section.id"
+                    class="section-item"
+                  >
+                    <view class="section-header" @click="toggleSection(section)">
+                      <text class="section-icon">{{ section.expanded ? '📖' : '📑' }}</text>
+                      <text class="section-name">{{ section.name }}</text>
+                    </view>
+
+                    <!-- 详细内容 -->
+                    <view v-if="section.expanded && section.contentBlocks" class="content-blocks">
+                      <view
+                        v-for="block in section.contentBlocks"
+                        :key="block.id"
+                        class="content-block"
+                      >
+                        <text class="block-content">{{ block.content }}</text>
+                      </view>
+                    </view>
+                  </view>
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, computed } from 'vue';
+import { get, post } from '../../utils/request';
+
+const newTopic = ref('');
+const creating = ref(false);
+const loadingTasks = ref(false);
+const confirming = ref(false);
+const tasks = ref<any[]>([]);
+const currentTask = ref<any>(null);
+const pendingData = ref<any>(null);
+const agentStatus = ref<any>(null);
+const selectedMode = ref<'progressive' | 'multi-agent'>('progressive');
+
+const statusText: Record<string, string> = {
+  pending: '等待中',
+  generating: '生成中',
+  completed: '已完成',
+  failed: '失败',
+};
+
+onMounted(() => {
+  loadTasks();
+});
+
+async function loadTasks() {
+  loadingTasks.value = true;
+  try {
+    const result = await get<any[]>('/ai-content/learning-path/list');
+    tasks.value = result || [];
+  } catch (e) {
+    console.error('加载任务失败:', e);
+  } finally {
+    loadingTasks.value = false;
+  }
+}
+
+async function createTask() {
+  if (!newTopic.value.trim() || creating.value) return;
+
+  creating.value = true;
+  try {
+    const task = await post<any>('/ai-content/learning-path/create', {
+      topic: newTopic.value.trim(),
+      mode: selectedMode.value,
+    });
+    tasks.value.unshift(task);
+    newTopic.value = '';
+    // 自动打开新创建的任务
+    viewTask(task.id);
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '创建失败', icon: 'none' });
+  } finally {
+    creating.value = false;
+  }
+}
+
+async function viewTask(id: number) {
+  try {
+    const result = await get<any>(`/ai-content/learning-path/${id}`);
+    currentTask.value = result;
+
+    // 初始化展开状态
+    if (result.subjects) {
+      result.subjects.forEach((s: any) => {
+        s.expanded = true;
+        if (s.chapters) {
+          s.chapters.forEach((c: any) => {
+            c.expanded = false;
+            if (c.sections) {
+              c.sections.forEach((sec: any) => {
+                sec.expanded = false;
+              });
+            }
+          });
+        }
+      });
+    }
+
+    // 如果是渐进模式,获取待确认数据
+    if (result.generationMode === 'progressive') {
+      await loadPendingData(id);
+    }
+
+    // 如果是多Agent模式,获取Agent状态
+    if (result.generationMode === 'multi-agent') {
+      await loadAgentStatus(id);
+    }
+
+    // 刷新任务状态
+    startPolling(id);
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '加载失败', icon: 'none' });
+  }
+}
+
+async function loadPendingData(taskId: number) {
+  try {
+    pendingData.value = await get<any>(`/ai-content/learning-path/${taskId}/pending`);
+  } catch (e) {
+    console.error('加载待确认数据失败:', e);
+  }
+}
+
+async function loadAgentStatus(taskId: number) {
+  try {
+    agentStatus.value = await get<any>(`/ai-content/learning-path/${taskId}/status`);
+  } catch (e) {
+    console.error('加载Agent状态失败:', e);
+  }
+}
+
+async function confirmStep(step: string) {
+  if (confirming.value) return;
+
+  confirming.value = true;
+  try {
+    let confirmedIds: number[] = [];
+
+    if (step === 'subjects' && pendingData.value) {
+      confirmedIds = pendingData.value.pendingSubjects.map((s: any) => s.id);
+    } else if (step === 'chapters' && pendingData.value) {
+      // 收集所有章节ID
+      Object.values(pendingData.value.pendingChapters).forEach((chapters: any) => {
+        confirmedIds.push(...chapters.map((c: any) => c.id));
+      });
+    } else if (step === 'sections' && pendingData.value) {
+      // 收集所有小节ID
+      Object.values(pendingData.value.pendingSections).forEach((sections: any) => {
+        confirmedIds.push(...sections.map((s: any) => s.id));
+      });
+    }
+
+    await post(`/ai-content/learning-path/${currentTask.value.id}/confirm`, {
+      step,
+      confirmedIds,
+      modifiedItems: [],
+    });
+
+    uni.showToast({ title: '确认成功,继续生成中...', icon: 'none' });
+
+    // 重新加载待确认数据
+    await loadPendingData(currentTask.value.id);
+
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '确认失败', icon: 'none' });
+  } finally {
+    confirming.value = false;
+  }
+}
+
+function getSubjectName(subjectId: number): string {
+  const subject = currentTask.value?.subjects?.find((s: any) => s.id === subjectId);
+  return subject?.name || '';
+}
+
+function getChapterName(chapterId: number): string {
+  for (const subject of currentTask.value?.subjects || []) {
+    const chapter = subject.chapters?.find((c: any) => c.id === chapterId);
+    if (chapter) return chapter.name;
+  }
+  return '';
+}
+
+let pollTimer: number | null = null;
+
+function startPolling(id: number) {
+  if (pollTimer) clearInterval(pollTimer);
+
+  pollTimer = setInterval(async () => {
+    try {
+      const result = await get<any>(`/ai-content/learning-path/${id}`);
+      currentTask.value = result;
+
+      // 更新列表中的任务
+      const idx = tasks.value.findIndex(t => t.id === id);
+      if (idx >= 0) {
+        tasks.value[idx] = result;
+      }
+
+      // 如果是渐进模式,刷新待确认数据
+      if (result.generationMode === 'progressive') {
+        await loadPendingData(id);
+      }
+
+      // 如果是多Agent模式,刷新Agent状态
+      if (result.generationMode === 'multi-agent') {
+        await loadAgentStatus(id);
+      }
+
+      // 如果完成,停止轮询
+      if (result.status === 'completed' || result.status === 'failed') {
+        if (pollTimer) clearInterval(pollTimer);
+        uni.showToast({
+          title: result.status === 'completed' ? '生成完成!' : '生成失败',
+          icon: 'none'
+        });
+      }
+    } catch (e) {
+      console.error('刷新失败:', e);
+    }
+  }, 5000) as unknown as number;
+}
+
+function closeDetail() {
+  currentTask.value = null;
+  pendingData.value = null;
+  agentStatus.value = null;
+  if (pollTimer) clearInterval(pollTimer);
+}
+
+function toggleSubject(subject: any) {
+  subject.expanded = !subject.expanded;
+}
+
+function toggleChapter(chapter: any) {
+  chapter.expanded = !chapter.expanded;
+}
+
+function toggleSection(section: any) {
+  section.expanded = !section.expanded;
+}
+
+function goBack() {
+  if (currentTask.value) {
+    closeDetail();
+  } else {
+    uni.navigateBack();
+  }
+}
+
+function formatDate(dateStr: string) {
+  const date = new Date(dateStr);
+  return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+.nav-bar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 88rpx;
+  background: #ffffff;
+  z-index: 100;
+  padding-top: 44rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
+}
+
+.nav-content {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 32rpx;
+  height: 88rpx;
+}
+
+.nav-left, .nav-right {
+  width: 80rpx;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #1f2937;
+}
+
+.page-title {
+  font-size: 34rpx;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.main-content {
+  padding: 132rpx 32rpx 32rpx;
+}
+
+.card {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.card-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.refresh-btn {
+  font-size: 32rpx;
+}
+
+.close-btn {
+  font-size: 48rpx;
+  color: #9ca3af;
+  line-height: 1;
+}
+
+.topic-input {
+  width: 100%;
+  min-height: 160rpx;
+  padding: 24rpx;
+  background: #f9fafb;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  color: #1f2937;
+  line-height: 1.6;
+  box-sizing: border-box;
+  margin-bottom: 24rpx;
+}
+
+/* 模式选择器 */
+.mode-selector {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 24rpx;
+}
+
+.mode-option {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  background: #f9fafb;
+  border-radius: 16rpx;
+  border: 2rpx solid transparent;
+  transition: all 0.2s;
+}
+
+.mode-option.active {
+  background: #eef2ff;
+  border-color: #6366f1;
+}
+
+.mode-icon {
+  font-size: 40rpx;
+  margin-right: 16rpx;
+}
+
+.mode-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.mode-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.mode-desc {
+  font-size: 22rpx;
+  color: #6b7280;
+}
+
+.create-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
+  border-radius: 16rpx;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.create-btn::after {
+  border: none;
+}
+
+.create-btn[disabled] {
+  background: #e5e7eb;
+}
+
+.create-btn text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+.loading, .empty {
+  text-align: center;
+  padding: 40rpx;
+  color: #9ca3af;
+  font-size: 28rpx;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.task-item {
+  padding: 24rpx;
+  background: #f9fafb;
+  border-radius: 16rpx;
+}
+
+.task-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.task-left {
+  flex: 1;
+}
+
+.task-topic {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #1f2937;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.task-meta {
+  display: flex;
+  gap: 12rpx;
+}
+
+.task-mode {
+  font-size: 20rpx;
+  padding: 4rpx 10rpx;
+  border-radius: 6rpx;
+  background: #e0e7ff;
+  color: #4f46e5;
+}
+
+.task-mode.multi-agent {
+  background: #fef3c7;
+  color: #d97706;
+}
+
+.task-status {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+}
+
+.task-status.pending {
+  background: #f3f4f6;
+  color: #6b7280;
+}
+
+.task-status.generating {
+  background: #fef3c7;
+  color: #d97706;
+}
+
+.task-status.completed {
+  background: #d1fae5;
+  color: #059669;
+}
+
+.task-status.failed {
+  background: #fee2e2;
+  color: #dc2626;
+}
+
+.task-progress {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 12rpx;
+}
+
+.progress-bar {
+  flex: 1;
+  height: 8rpx;
+  background: #e5e7eb;
+  border-radius: 4rpx;
+  overflow: hidden;
+}
+
+.progress-bar.large {
+  height: 12rpx;
+}
+
+.progress-bar.small {
+  height: 6rpx;
+  width: 120rpx;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
+  border-radius: 4rpx;
+  transition: width 0.3s ease;
+}
+
+.progress-text {
+  font-size: 24rpx;
+  color: #6b7280;
+  min-width: 80rpx;
+  text-align: right;
+}
+
+.task-date {
+  font-size: 22rpx;
+  color: #9ca3af;
+}
+
+/* 详情页 */
+.task-detail {
+  margin-top: 24rpx;
+}
+
+.overall-progress {
+  margin-bottom: 32rpx;
+  padding: 24rpx;
+  background: #f9fafb;
+  border-radius: 16rpx;
+}
+
+.progress-info {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+
+.progress-info text {
+  font-size: 26rpx;
+  color: #6b7280;
+}
+
+/* Agent状态 */
+.agent-status {
+  margin-bottom: 24rpx;
+  padding: 20rpx;
+  background: #fffbeb;
+  border-radius: 12rpx;
+}
+
+.agent-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #92400e;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.agent-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+
+.agent-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+
+.agent-name {
+  font-size: 24rpx;
+  color: #78350f;
+  min-width: 120rpx;
+}
+
+.agent-progress {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+
+.agent-percent {
+  font-size: 22rpx;
+  color: #92400e;
+  min-width: 60rpx;
+  text-align: right;
+}
+
+/* 确认面板 */
+.confirm-panel {
+  margin-bottom: 24rpx;
+  padding: 20rpx;
+  background: #f0fdf4;
+  border-radius: 12rpx;
+  border: 2rpx solid #86efac;
+}
+
+.confirm-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #166534;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.confirm-section {
+  margin-bottom: 16rpx;
+}
+
+.confirm-step {
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #15803d;
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+.confirm-hint {
+  font-size: 24rpx;
+  color: #6b7280;
+  display: block;
+  margin-top: 8rpx;
+}
+
+.confirm-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8rpx;
+  margin-bottom: 16rpx;
+}
+
+.confirm-item {
+  display: flex;
+  align-items: center;
+  padding: 8rpx 16rpx;
+  background: #ffffff;
+  border-radius: 8rpx;
+  border: 1rpx solid #d1d5db;
+}
+
+.item-icon {
+  font-size: 24rpx;
+  margin-right: 8rpx;
+}
+
+.item-name {
+  font-size: 24rpx;
+  color: #374151;
+}
+
+.subject-chapters, .chapter-sections {
+  margin-bottom: 16rpx;
+}
+
+.subject-label, .chapter-label {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #374151;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.confirm-btn {
+  width: 100%;
+  height: 80rpx;
+  background: linear-gradient(135deg, #059669 0%, #10b981 100%);
+  border-radius: 12rpx;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.confirm-btn::after {
+  border: none;
+}
+
+.confirm-btn[disabled] {
+  background: #d1d5db;
+}
+
+.confirm-btn text {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+/* 学科列表 */
+.subjects {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.subject-header {
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  background: #f3f4f6;
+  border-radius: 12rpx;
+}
+
+.subject-icon, .chapter-icon, .section-icon {
+  font-size: 32rpx;
+  margin-right: 16rpx;
+}
+
+.subject-name {
+  flex: 1;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.subject-status {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+}
+
+.subject-status.pending {
+  background: #f3f4f6;
+  color: #6b7280;
+}
+
+.subject-status.completed {
+  background: #d1fae5;
+  color: #059669;
+}
+
+.chapters {
+  margin-left: 48rpx;
+  padding-left: 24rpx;
+  border-left: 2rpx solid #e5e7eb;
+}
+
+.chapter-header {
+  display: flex;
+  align-items: center;
+  padding: 16rpx;
+}
+
+.chapter-name {
+  flex: 1;
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #374151;
+}
+
+.sections {
+  margin-left: 32rpx;
+  padding-left: 16rpx;
+  border-left: 2rpx solid #e5e7eb;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  padding: 12rpx;
+}
+
+.section-name {
+  flex: 1;
+  font-size: 24rpx;
+  color: #6b7280;
+}
+
+.content-blocks {
+  margin-left: 48rpx;
+  padding: 16rpx;
+  background: #f9fafb;
+  border-radius: 12rpx;
+  margin-bottom: 12rpx;
+}
+
+.block-content {
+  font-size: 26rpx;
+  color: #374151;
+  line-height: 1.8;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+</style>

+ 6 - 7
my-uniapp-vue3/src/utils/request.ts

@@ -1,6 +1,9 @@
-// API 基础配置
+// API 基础配置 - 开发环境直接使用后端 3000 端口
 // @ts-ignore
 // @ts-ignore
-const BASE_URL = (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_BASE_URL) || '/api';
+const isDevEnv = typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV;
+const BASE_URL = isDevEnv
+  ? 'http://localhost:3000/api'
+  : (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_BASE_URL) || '/api';
 
 
 // 开发环境使用本地 API,生产环境使用线上 API
 // 开发环境使用本地 API,生产环境使用线上 API
 const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
 const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
@@ -12,12 +15,8 @@ export function getFullUrl(path: string): string {
     return path;
     return path;
   }
   }
   // 开发环境使用完整的后端 URL
   // 开发环境使用完整的后端 URL
-  // @ts-ignore
-  const isDevEnv = typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV;
   if (isDevEnv) {
   if (isDevEnv) {
-    // @ts-ignore
-    const backendUrl = (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_BASE_URL) || 'http://localhost:3000';
-    return backendUrl + path;
+    return 'http://localhost:3000' + path;
   }
   }
   // 生产环境使用相对路径
   // 生产环境使用相对路径
   return path;
   return path;

+ 11 - 0
server/del.js

@@ -0,0 +1,11 @@
+const { PrismaClient } = require("@prisma/client");
+const prisma = new PrismaClient();
+async function main() {
+  await prisma.contentBlock.deleteMany({where:{section:{chapter:{subject:{learningPathId:4}}}}});
+  await prisma.section.deleteMany({where:{chapter:{subject:{learningPathId:4}}}}});
+  await prisma.chapter.deleteMany({where:{subject:{learningPathId:4}}}});
+  await prisma.subject.deleteMany({where:{learningPathId:4}});
+  await prisma.learningPath.delete({where:{id:4}});
+  console.log("Done");
+}
+main().then(()=>prisma.$disconnect()).catch(e=>{console.error(e);prisma.$disconnect();process.exit(1);});

+ 33 - 1
server/package-lock.json

@@ -12,6 +12,7 @@
         "@koa/cors": "^5.0.0",
         "@koa/cors": "^5.0.0",
         "@koa/router": "^12.0.1",
         "@koa/router": "^12.0.1",
         "@prisma/client": "^6.19.3",
         "@prisma/client": "^6.19.3",
+        "@types/ws": "^8.18.1",
         "axios": "^1.7.2",
         "axios": "^1.7.2",
         "crypto-js": "^4.2.0",
         "crypto-js": "^4.2.0",
         "dotenv": "^16.4.5",
         "dotenv": "^16.4.5",
@@ -23,7 +24,8 @@
         "koa-static": "^5.0.0",
         "koa-static": "^5.0.0",
         "mysql2": "^3.20.0",
         "mysql2": "^3.20.0",
         "prisma": "^6.19.3",
         "prisma": "^6.19.3",
-        "uuid": "^9.0.1"
+        "uuid": "^9.0.1",
+        "ws": "^8.20.0"
       },
       },
       "devDependencies": {
       "devDependencies": {
         "@types/crypto-js": "^4.2.2",
         "@types/crypto-js": "^4.2.2",
@@ -974,6 +976,15 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/@types/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/accepts": {
     "node_modules/accepts": {
       "version": "1.3.8",
       "version": "1.3.8",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@@ -2912,6 +2923,27 @@
         "which": "bin/which"
         "which": "bin/which"
       }
       }
     },
     },
+    "node_modules/ws": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz",
+      "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/yn": {
     "node_modules/yn": {
       "version": "3.1.1",
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",
       "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",

+ 3 - 1
server/package.json

@@ -13,6 +13,7 @@
     "@koa/cors": "^5.0.0",
     "@koa/cors": "^5.0.0",
     "@koa/router": "^12.0.1",
     "@koa/router": "^12.0.1",
     "@prisma/client": "^6.19.3",
     "@prisma/client": "^6.19.3",
+    "@types/ws": "^8.18.1",
     "axios": "^1.7.2",
     "axios": "^1.7.2",
     "crypto-js": "^4.2.0",
     "crypto-js": "^4.2.0",
     "dotenv": "^16.4.5",
     "dotenv": "^16.4.5",
@@ -24,7 +25,8 @@
     "koa-static": "^5.0.0",
     "koa-static": "^5.0.0",
     "mysql2": "^3.20.0",
     "mysql2": "^3.20.0",
     "prisma": "^6.19.3",
     "prisma": "^6.19.3",
-    "uuid": "^9.0.1"
+    "uuid": "^9.0.1",
+    "ws": "^8.20.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/crypto-js": "^4.2.2",
     "@types/crypto-js": "^4.2.2",

+ 98 - 0
server/prisma/schema.prisma

@@ -219,3 +219,101 @@ model Template {
 
 
   @@index([category])
   @@index([category])
 }
 }
+
+// 学习路径任务
+model LearningPath {
+  id          Int       @id @default(autoincrement())
+  userId      Int?
+  topic       String    @db.Text // 用户输入的主题
+  status      String    @default("pending") // pending, generating, completed, failed
+  progress    Int       @default(0) // 0-100 进度百分比
+  totalCount  Int       @default(0) // 总内容块数
+  completedCount Int     @default(0) // 已完成内容块数
+  errorMsg    String?   @db.Text // 错误信息
+
+  // 生成模式: progressive(渐进确认) | multi-agent(多Agent并行)
+  generationMode String  @default("progressive")
+  // 当前步骤(渐进模式): pending, subjects, chapters, sections, content, completed
+  currentStep   String  @default("pending")
+  // 多Agent模式状态: 存储各Agent的进度JSON
+  agentStatus   String? @db.Text
+
+  createdAt   DateTime  @default(now())
+  updatedAt   DateTime  @updatedAt
+
+  subjects    Subject[]
+
+  @@index([userId, status])
+}
+
+// 学科(大类)
+model Subject {
+  id            Int       @id @default(autoincrement())
+  learningPathId Int
+  name          String    // 学科名称,如"计算机原理"
+  orderIndex    Int       @default(0) // 排序
+  status        String    @default("pending") // pending, generating, completed
+  aiResponse    String?   @db.Text // AI返回的原始内容
+  // 确认状态(渐进模式): pending, confirmed, regenerating
+  confirmationStatus String @default("pending")
+  createdAt     DateTime  @default(now())
+  updatedAt     DateTime  @updatedAt
+
+  learningPath  LearningPath @relation(fields: [learningPathId], references: [id], onDelete: Cascade)
+  chapters      Chapter[]
+
+  @@index([learningPathId, orderIndex])
+}
+
+// 章节
+model Chapter {
+  id          Int       @id @default(autoincrement())
+  subjectId   Int
+  name        String    // 章节名称
+  orderIndex  Int       @default(0)
+  status      String    @default("pending")
+  aiResponse  String?   @db.Text
+  // 确认状态(渐进模式): pending, confirmed, regenerating
+  confirmationStatus String @default("pending")
+  createdAt   DateTime  @default(now())
+  updatedAt   DateTime  @updatedAt
+
+  subject     Subject   @relation(fields: [subjectId], references: [id], onDelete: Cascade)
+  sections    Section[]
+
+  @@index([subjectId, orderIndex])
+}
+
+// 小节
+model Section {
+  id          Int       @id @default(autoincrement())
+  chapterId   Int
+  name        String    // 小节名称
+  orderIndex  Int       @default(0)
+  status      String    @default("pending")
+  aiResponse  String?   @db.Text
+  // 确认状态(渐进模式): pending, confirmed, regenerating
+  confirmationStatus String @default("pending")
+  createdAt   DateTime  @default(now())
+  updatedAt   DateTime  @updatedAt
+
+  chapter     Chapter   @relation(fields: [chapterId], references: [id], onDelete: Cascade)
+  contentBlocks ContentBlock[]
+
+  @@index([chapterId, orderIndex])
+}
+
+// 内容块(详细内容)
+model ContentBlock {
+  id          Int       @id @default(autoincrement())
+  sectionId   Int
+  content     String    @db.Text // 详细内容
+  orderIndex  Int       @default(0)
+  wordCount   Int       @default(0)
+  createdAt   DateTime  @default(now())
+  updatedAt   DateTime  @updatedAt
+
+  section     Section   @relation(fields: [sectionId], references: [id], onDelete: Cascade)
+
+  @@index([sectionId, orderIndex])
+}

+ 156 - 5
server/src/modules/ai-content/ai-content.controller.ts

@@ -4,9 +4,162 @@
 
 
 import Router from '@koa/router';
 import Router from '@koa/router';
 import { aiContentService } from './ai-content.service';
 import { aiContentService } from './ai-content.service';
+import { createLearningPath, getLearningPathDetail, getUserTasks, getPendingConfirmation, confirmAndContinue, regenerateNode, getAgentStatus } from './learning-path.service';
 
 
 const router = new Router();
 const router = new Router();
 
 
+// ============ 学习路径 API ============
+
+// 创建学习路径任务
+router.post('/learning-path/create', async (ctx) => {
+  const { topic, mode } = ctx.request.body as { topic: string; mode?: string };
+  if (!topic || topic.trim().length === 0) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '请输入学习主题' };
+    return;
+  }
+
+  // 验证模式
+  const generationMode = mode === 'multi-agent' ? 'multi-agent' : 'progressive';
+
+  // 获取用户ID(如果有)
+  const token = ctx.request.headers.authorization?.replace('Bearer ', '');
+  let userId: number | null = null;
+  if (token) {
+    try {
+      const jwt = require('jsonwebtoken');
+      const decoded = jwt.verify(token, 'my-jwt-secret-key-2024');
+      userId = decoded.userId || decoded.id || null;
+    } catch (e) {}
+  }
+
+  const task = await createLearningPath(userId, topic, generationMode);
+  ctx.body = { code: 0, message: 'success', data: task };
+});
+
+// 获取用户的学习任务列表(要放在 /:id 前面,否则 /list 会被匹配为 id=list)
+router.get('/learning-path/list', async (ctx) => {
+  const token = ctx.request.headers.authorization?.replace('Bearer ', '');
+  let userId: number | null = null;
+  if (token) {
+    try {
+      const jwt = require('jsonwebtoken');
+      const decoded = jwt.verify(token, 'my-jwt-secret-key-2024');
+      userId = decoded.userId || decoded.id || null;
+    } catch (e) {}
+  }
+
+  const tasks = await getUserTasks(userId);
+  ctx.body = { code: 0, message: 'success', data: tasks };
+});
+
+// 获取学习路径详情(要放在 /list 后面)
+router.get('/learning-path/:id', async (ctx) => {
+  const id = parseInt(ctx.params.id);
+  if (isNaN(id)) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '无效的任务ID' };
+    return;
+  }
+  const task = await getLearningPathDetail(id);
+  if (!task) {
+    ctx.status = 404;
+    ctx.body = { code: 404, message: '任务不存在' };
+    return;
+  }
+  ctx.body = { code: 0, message: 'success', data: task };
+});
+
+// 获取渐进模式待确认状态
+router.get('/learning-path/:id/pending', async (ctx) => {
+  const id = parseInt(ctx.params.id);
+  if (isNaN(id)) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '无效的任务ID' };
+    return;
+  }
+  const pending = await getPendingConfirmation(id);
+  if (!pending) {
+    ctx.status = 404;
+    ctx.body = { code: 404, message: '任务不存在' };
+    return;
+  }
+  ctx.body = { code: 0, message: 'success', data: pending };
+});
+
+// 确认并继续生成(渐进模式)
+router.post('/learning-path/:id/confirm', async (ctx) => {
+  const id = parseInt(ctx.params.id);
+  if (isNaN(id)) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '无效的任务ID' };
+    return;
+  }
+  const { step, confirmedIds, modifiedItems } = ctx.request.body as {
+    step: string;
+    confirmedIds: number[];
+    modifiedItems?: { id: number; name: string }[];
+  };
+  if (!step || !confirmedIds) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '缺少必要参数' };
+    return;
+  }
+  try {
+    const result = await confirmAndContinue(id, step, confirmedIds, modifiedItems);
+    ctx.body = { code: 0, message: 'success', data: result };
+  } catch (e: any) {
+    ctx.status = 500;
+    ctx.body = { code: 500, message: e.message };
+  }
+});
+
+// 重新生成节点(渐进模式)
+router.post('/learning-path/:id/regenerate', async (ctx) => {
+  const id = parseInt(ctx.params.id);
+  if (isNaN(id)) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '无效的任务ID' };
+    return;
+  }
+  const { type, nodeId, instruction } = ctx.request.body as {
+    type: 'chapter' | 'section';
+    nodeId: number;
+    instruction?: string;
+  };
+  if (!type || !nodeId) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '缺少必要参数' };
+    return;
+  }
+  try {
+    const result = await regenerateNode(id, type, nodeId, instruction);
+    ctx.body = { code: 0, message: 'success', data: result };
+  } catch (e: any) {
+    ctx.status = 500;
+    ctx.body = { code: 500, message: e.message };
+  }
+});
+
+// 获取多Agent模式状态
+router.get('/learning-path/:id/status', async (ctx) => {
+  const id = parseInt(ctx.params.id);
+  if (isNaN(id)) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '无效的任务ID' };
+    return;
+  }
+  const status = await getAgentStatus(id);
+  if (!status) {
+    ctx.status = 404;
+    ctx.body = { code: 404, message: '任务不存在' };
+    return;
+  }
+  ctx.body = { code: 0, message: 'success', data: status };
+});
+
+// ============ 原有的 API ============
+
 // 智能意图识别
 // 智能意图识别
 router.get('/intent/recognize', async (ctx) => {
 router.get('/intent/recognize', async (ctx) => {
   const { input } = ctx.query;
   const { input } = ctx.query;
@@ -28,13 +181,11 @@ router.get('/content-types/all', async (ctx) => {
 
 
 // 统一内容生成接口
 // 统一内容生成接口
 router.post('/generate', async (ctx) => {
 router.post('/generate', async (ctx) => {
-  const { type, industry, prompt, targetLength } = ctx.request.body as {
-    type: string;
-    industry: string;
+  const { prompt, targetLength } = ctx.request.body as {
     prompt: string;
     prompt: string;
-    targetLength: number;
+    targetLength?: number;
   };
   };
-  const result = await aiContentService.generateContent(type, industry, prompt, targetLength);
+  const result = await aiContentService.generateContent(prompt, targetLength || 2000);
   ctx.body = { code: 0, message: 'success', data: result };
   ctx.body = { code: 0, message: 'success', data: result };
 });
 });
 
 

+ 112 - 205
server/src/modules/ai-content/ai-content.service.ts

@@ -32,8 +32,21 @@ const languages = ['中文', '英语', '日语', '韩语', '法语', '德语', '
 // 质量评分维度
 // 质量评分维度
 const qualityDimensions = ['fluency', 'naturalness', 'emotion_consistency', 'topic_adherence', 'structural_integrity'];
 const qualityDimensions = ['fluency', 'naturalness', 'emotion_consistency', 'topic_adherence', 'structural_integrity'];
 
 
-// 默认模型
-const DEFAULT_MODEL = 'qwen-plus';
+// 可用模型列表(已测试可用)
+const AVAILABLE_MODELS = [
+  'qwen-plus',
+  'qwen-max',
+  'qwen-turbo',
+  'MiniMax-M2.5',
+  'tongyi-xiaomi-analysis-pro',
+  'tongyi-xiaomi-analysis-flash',
+  'MiniMax-M2.1',
+];
+
+// 随机选择模型
+function getRandomModel(): string {
+  return AVAILABLE_MODELS[Math.floor(Math.random() * AVAILABLE_MODELS.length)];
+}
 
 
 export class AIContentService {
 export class AIContentService {
   private apiKey: string;
   private apiKey: string;
@@ -41,7 +54,7 @@ export class AIContentService {
 
 
   constructor() {
   constructor() {
     this.apiKey = config.dashscope.apiKey || '';
     this.apiKey = config.dashscope.apiKey || '';
-    this.model = DEFAULT_MODEL;
+    this.model = getRandomModel();
   }
   }
 
 
   /**
   /**
@@ -52,11 +65,7 @@ export class AIContentService {
       throw new Error('未配置 AI API Key');
       throw new Error('未配置 AI API Key');
     }
     }
 
 
-    const messages: any[] = [];
-    if (systemPrompt) {
-      messages.push({ role: 'system', content: systemPrompt });
-    }
-    messages.push({ role: 'user', content: prompt });
+    const fullPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
 
 
     try {
     try {
       const response = await axios.post(
       const response = await axios.post(
@@ -64,7 +73,7 @@ export class AIContentService {
         {
         {
           model: this.model,
           model: this.model,
           input: {
           input: {
-            messages,
+            prompt: fullPrompt,
           },
           },
           parameters: {
           parameters: {
             result_format: 'message',
             result_format: 'message',
@@ -120,11 +129,7 @@ export class AIContentService {
       throw new Error('未配置 AI API Key');
       throw new Error('未配置 AI API Key');
     }
     }
 
 
-    const messages: any[] = [];
-    if (systemPrompt) {
-      messages.push({ role: 'system', content: systemPrompt });
-    }
-    messages.push({ role: 'user', content: prompt });
+    const fullPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
 
 
     try {
     try {
       const response = await axios.post(
       const response = await axios.post(
@@ -132,7 +137,7 @@ export class AIContentService {
         {
         {
           model: this.model,
           model: this.model,
           input: {
           input: {
-            messages,
+            prompt: fullPrompt,
           },
           },
           parameters: {
           parameters: {
             result_format: 'message',
             result_format: 'message',
@@ -232,40 +237,69 @@ export class AIContentService {
   }
   }
 
 
   /**
   /**
-   * 统一内容生成 - 根据类型自动选择生成策略
+   * 统一内容生成
    */
    */
-  async generateContent(type: string, industry: string, prompt: string, targetLength: number = 2000) {
-    // 根据内容类型选择不同的生成策略
-    const novelTypes = ['小说', '故事', '剧本'];
-    const marketingTypes = ['产品介绍', '广告文案', '朋友圈', '小红书', '抖音脚本'];
-    const educationTypes = ['课件', '培训', '教程', '知识科普'];
-    const businessTypes = ['销售话术', '客服话术', '商务邮件'];
-    const mediaTypes = ['新闻播报', '天气预报', '体育解说'];
-    const otherTypes = ['诗歌', '散文'];
+  async generateContent(prompt: string, targetLength: number = 2000) {
+    // 系统性知识展开
+    const finalPrompt = `你是一位专业的老师。请围绕用户的主题,系统性地讲解这个知识点。
 
 
-    let content = '';
+用户主题:${prompt}
 
 
-    if (novelTypes.includes(type)) {
-      // 小说类:生成分章节的长篇内容
-      content = await this.generateNovel(prompt, targetLength);
-    } else if (marketingTypes.includes(type)) {
-      // 营销类:生成营销文案
-      content = await this.generateMarketing(type, prompt, industry, targetLength);
-    } else if (educationTypes.includes(type)) {
-      // 教育类:生成教学内容
-      content = await this.generateEducation(type, prompt, industry, targetLength);
-    } else if (businessTypes.includes(type)) {
-      // 商务类:生成话术或邮件
-      content = await this.generateBusiness(type, prompt, industry, targetLength);
-    } else if (mediaTypes.includes(type)) {
-      // 媒体类:生成播报内容
-      content = await this.generateMedia(type, prompt, industry);
-    } else {
-      // 其他类型:通用生成
-      content = await this.generateGeneric(type, prompt, targetLength);
+要求:
+1. 首先分析这个主题涉及的核心领域和知识体系
+2. 按照"大类 -> 小类 -> 具体知识点"的层次结构展开讲解
+3. 每个知识点都要讲清楚"是什么"、"为什么"、"怎么用"
+4. 内容要准确、全面、深入浅出
+5. 直接返回正文内容,用清晰的章节标题组织结构
+6. 目标字数:${targetLength}字左右,如果内容有价值可以超出`;
+
+    console.log('🤖 [AI内容生成] 最终Prompt:', finalPrompt);
+    console.log('🤖 [AI内容生成] 使用模型:', this.model);
+
+    const content = await this.callLLM(finalPrompt);
+    return {
+      content,
+      type: '通用',
+      industry: '通用',
+      wordCount: content.length,
+      debug: {
+        model: this.model,
+        finalPrompt
+      }
+    };
+  }
+
+  /**
+   * AI自动判断内容类型和行业
+   */
+  private async detectTypeAndIndustry(prompt: string): Promise<{ type: string; industry: string }> {
+    const detectPrompt = `分析以下内容需求,判断其类型和所属行业。
+
+需求内容:${prompt}
+
+请以JSON格式返回:
+{"type": "内容类型", "industry": "所属行业"}
+
+内容类型选项:小说、故事、剧本、诗歌、散文、营销文案、教育内容、商务内容、媒体内容
+行业选项:通用、医疗健康、教育培训、金融服务、电子商务、餐饮美食、法律服务、新闻媒体
+
+只返回JSON,不要其他内容。`;
+
+    try {
+      const response = await this.callLLM(detectPrompt);
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const parsed = JSON.parse(jsonMatch[0]);
+        return {
+          type: parsed.type || '通用',
+          industry: parsed.industry || '通用',
+        };
+      }
+    } catch (e) {
+      console.log('类型检测失败,使用默认类型');
     }
     }
 
 
-    return { content, type, industry, wordCount: content.length };
+    return { type: '通用', industry: '通用' };
   }
   }
 
 
   /**
   /**
@@ -337,204 +371,77 @@ export class AIContentService {
   /**
   /**
    * 生成营销文案
    * 生成营销文案
    */
    */
-  private async generateMarketing(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
-    const templates: Record<string, string> = {
-      '产品介绍': `为以下产品写一篇详细介绍文案:
-
-产品信息:${prompt}
-行业:${industry}
-
-要求:
-1. 结构清晰,包含产品特点、优势、适用场景
-2. 语言生动,有感染力
-3. 字数:${targetLength}字左右`,
-      '广告文案': `写一条吸引人的广告文案:
-
-产品/活动:${prompt}
-行业:${industry}
+  private async generateMarketing(prompt: string, targetLength: number): Promise<string> {
+    const marketingPrompt = `根据以下需求,写一篇营销文案:
 
 
-要求:
-1. 简短有力,突出卖点
-2. 有吸引力,让人想购买
-3. 直接返回文案内容`,
-      '朋友圈': `写一条朋友圈文案:
-
-内容主题:${prompt}
-
-要求:
-1. 轻松自然,有生活气息
-2. 符合朋友圈风格
-3. 可以带emoji`,
-      '小红书': `写一篇小红书种草文案:
-
-产品/体验:${prompt}
-
-要求:
-1. 生动有趣,吸引人
-2. 包含真实体验感
-3. 带话题标签
-4. 字数:${targetLength}字左右`,
-      '抖音脚本': `写一个抖音短视频脚本:
-
-内容:${prompt}
+需求:${prompt}
 
 
 要求:
 要求:
-1. 结构:开头-内容-结尾
-2. 节奏快,有爆点
-3. 适合短视频节奏`,
-    };
-
-    const templatePrompt = templates[type] || `根据以下内容,生成合适的文案:
-
-${prompt}
+1. 语言生动,有感染力
+2. 符合目标受众喜好
+3. 字数:${targetLength}字左右
+4. 直接返回正文内容,不要其他说明`;
 
 
-要求:字数${targetLength}字左右,直接返回内容`;
-
-    return await this.callLLM(templatePrompt);
+    return await this.callLLM(marketingPrompt);
   }
   }
 
 
   /**
   /**
    * 生成教育培训内容
    * 生成教育培训内容
    */
    */
-  private async generateEducation(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
-    const templates: Record<string, string> = {
-      '课件': `为以下主题制作课件内容:
-
-主题:${prompt}
-行业:${industry}
-
-要求:
-1. 结构清晰:导入-讲解-总结
-2. 重点突出
-3. 字数:${targetLength}字左右`,
-      '培训': `编写培训内容:
-
-主题:${prompt}
-对象:培训学员
-
-要求:
-1. 实用性强,易于理解
-2. 包含案例和练习
-3. 字数:${targetLength}字左右`,
-      '教程': `写一篇教程:
+  private async generateEducation(prompt: string, targetLength: number): Promise<string> {
+    const educationPrompt = `根据以下需求,生成教育培训内容:
 
 
-主题:${prompt}
-
-要求:
-1. 步骤清晰
-2. 易于跟着操作
-3. 字数:${targetLength}字左右`,
-      '知识科普': `写一篇科普文章:
-
-主题:${prompt}
+需求:${prompt}
 
 
 要求:
 要求:
-1. 语言通俗易懂
-2. 有趣味性
-3. 字数:${targetLength}字左右`,
-    };
-
-    const templatePrompt = templates[type] || `根据以下主题生成教学内容:
-
-${prompt}
+1. 结构清晰,易于理解
+2. 实用性强
+3. 字数:${targetLength}字左右
+4. 直接返回正文内容`;
 
 
-要求:字数${targetLength}字左右`;
-    return await this.callLLM(templatePrompt);
+    return await this.callLLM(educationPrompt);
   }
   }
 
 
   /**
   /**
    * 生成商务内容
    * 生成商务内容
    */
    */
-  private async generateBusiness(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
-    const templates: Record<string, string> = {
-      '销售话术': `写一段销售话术:
-
-产品:${prompt}
-行业:${industry}
-
-要求:
-1. 开场白-产品介绍-处理异议-促成
-2. 专业且有说服力
-3. 字数:${targetLength}字左右`,
-      '客服话术': `写一段客服话术:
-
-场景:${prompt}
-行业:${industry}
+  private async generateBusiness(prompt: string, targetLength: number): Promise<string> {
+    const businessPrompt = `根据以下需求,生成商务内容:
 
 
-要求:
-1. 礼貌耐心
-2. 解决问题导向
-3. 字数:${targetLength}字左右`,
-      '商务邮件': `写一封商务邮件:
-
-背景:${prompt}
-行业:${industry}
+需求:${prompt}
 
 
 要求:
 要求:
-1. 格式规范
-2. 语言专业得体
-3. 目的明确`,
-    };
-
-    const templatePrompt = templates[type] || `生成商务内容:
-
-${prompt}
+1. 语言专业得体
+2. 目的明确
+3. 字数:${targetLength}字左右
+4. 直接返回正文内容`;
 
 
-要求:字数${targetLength}字左右`;
-    return await this.callLLM(templatePrompt);
+    return await this.callLLM(businessPrompt);
   }
   }
 
 
   /**
   /**
    * 生成媒体内容
    * 生成媒体内容
    */
    */
-  private async generateMedia(type: string, prompt: string, industry: string): Promise<string> {
-    const templates: Record<string, string> = {
-      '新闻播报': `写一段新闻播报稿:
-
-新闻内容:${prompt}
-
-要求:
-1. 语言正式、简洁
-2. 客观准确
-3. 适合朗读`,
-      '天气预报': `写一段天气预报稿:
-
-地区和天气:${prompt}
+  private async generateMedia(prompt: string): Promise<string> {
+    const mediaPrompt = `根据以下需求,生成媒体播报内容:
 
 
-要求:
-1. 语言清晰
-2. 包含温度、天气状况、建议
-3. 适合播报`,
-      '体育解说': `写一段体育解说稿:
-
-比赛内容:${prompt}
+需求:${prompt}
 
 
 要求:
 要求:
-1. 生动有趣
-2. 专业术语准确
-3. 节奏感强`,
-    };
-
-    const templatePrompt = templates[type] || `生成媒体内容:
+1. 语言清晰流畅
+2. 适合朗读或播报
+3. 直接返回正文内容`;
 
 
-${prompt}`;
-    return await this.callLLM(templatePrompt);
+    return await this.callLLM(mediaPrompt);
   }
   }
 
 
   /**
   /**
    * 通用生成
    * 通用生成
    */
    */
-  private async generateGeneric(type: string, prompt: string, targetLength: number): Promise<string> {
-    const templatePrompt = `以"${type}"的格式,生成内容:
-
-需求:${prompt}
-
-要求:
-1. 符合${type}的特点
-2. 字数:${targetLength}字左右
-3. 直接返回正文内容`;
+  private async generateGeneric(prompt: string, targetLength: number): Promise<string> {
+    const genericPrompt = `请用中文生成内容:${prompt},大约${targetLength}字,直接返回内容不要加标题`;
 
 
-    return await this.callLLM(templatePrompt);
+    return await this.callLLM(genericPrompt);
   }
   }
 
 
   /**
   /**

+ 926 - 0
server/src/modules/ai-content/learning-path.service.ts

@@ -0,0 +1,926 @@
+/**
+ * 学习路径生成服务
+ * 支持两种模式:
+ * 1. progressive(渐进确认) - 用户在每步确认后继续
+ * 2. multi-agent(多Agent并行) - 全自动并行生成
+ */
+
+import axios from 'axios';
+import { config } from '../../config';
+import { prisma } from '../../models';
+
+const AVAILABLE_MODELS = [
+  'qwen-plus',
+  'qwen-max',
+  'qwen-turbo',
+  'MiniMax-M2.5',
+  'tongyi-xiaomi-analysis-pro',
+  'tongyi-xiaomi-analysis-flash',
+  'MiniMax-M2.1',
+];
+
+function getRandomModel(): string {
+  return AVAILABLE_MODELS[Math.floor(Math.random() * AVAILABLE_MODELS.length)];
+}
+
+/**
+ * 调用 DashScope API
+ */
+async function callLLM(prompt: string): Promise<string> {
+  const apiKey = config.dashscope.apiKey;
+  if (!apiKey) {
+    throw new Error('未配置 AI API Key');
+  }
+
+  try {
+    const response = await axios.post(
+      'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
+      {
+        model: getRandomModel(),
+        input: { prompt },
+        parameters: { result_format: 'message' },
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${apiKey}`,
+          'Content-Type': 'application/json',
+        },
+        timeout: 180000,
+      }
+    );
+
+    const data = response.data;
+    if (data.code) {
+      throw new Error(data.message || `AI 调用失败: ${data.code}`);
+    }
+
+    return data.output?.choices?.[0]?.message?.content || '';
+  } catch (error: any) {
+    console.error('❌ LLM 调用失败:', error.message);
+    throw new Error(error.message || 'AI 生成失败');
+  }
+}
+
+/**
+ * 解析 AI 返回的 JSON 列表
+ */
+function parseJsonList(aiResponse: string): string[] {
+  const jsonMatch = aiResponse.match(/\[[\s\S]*\]/);
+  if (jsonMatch) {
+    try {
+      const parsed = JSON.parse(jsonMatch[0]);
+      if (Array.isArray(parsed)) {
+        return parsed.map(item => {
+          if (typeof item === 'string') return item;
+          if (typeof item === 'object' && item !== null) {
+            return item.title || item.name || item.chapter_title || item.section_title || item.section || item.chapter || JSON.stringify(item);
+          }
+          return String(item);
+        });
+      }
+    } catch (e) {}
+  }
+
+  const lines = aiResponse.split(/[,,\n]/).filter(line => line.trim().length > 0);
+  return lines.map(line => line.replace(/^[\d一二三四五六七八九十]+[.、::]\s*/, '').trim()).filter(line => line.length > 0);
+}
+
+// ============ 通用函数 ============
+
+/**
+ * 创建学习路径任务
+ * @param userId 用户ID
+ * @param topic 学习主题
+ * @param mode 生成模式: progressive | multi-agent
+ */
+export async function createLearningPath(userId: number | null, topic: string, mode: 'progressive' | 'multi-agent' = 'progressive') {
+  const task = await prisma.learningPath.create({
+    data: {
+      userId,
+      topic,
+      status: 'pending',
+      progress: 0,
+      generationMode: mode,
+      currentStep: 'pending',
+    },
+  });
+
+  // 根据模式选择生成方式
+  if (mode === 'multi-agent') {
+    // 多Agent模式:直接开始全量生成
+    generateMultiAgent(task.id).catch(error => {
+      console.error('❌ 多Agent生成失败:', error);
+      prisma.learningPath.update({
+        where: { id: task.id },
+        data: { status: 'failed', errorMsg: error.message },
+      }).catch(console.error);
+    });
+  } else {
+    // 渐进模式:先生成学科列表等待用户确认
+    generateSubjectsStep(task.id).catch(error => {
+      console.error('❌ 学科生成失败:', error);
+      prisma.learningPath.update({
+        where: { id: task.id },
+        data: { status: 'failed', errorMsg: error.message },
+      }).catch(console.error);
+    });
+  }
+
+  return task;
+}
+
+/**
+ * 获取学习路径详情
+ */
+export async function getLearningPathDetail(id: number) {
+  const task = await prisma.learningPath.findUnique({
+    where: { id },
+    include: {
+      subjects: {
+        orderBy: { orderIndex: 'asc' },
+        include: {
+          chapters: {
+            orderBy: { orderIndex: 'asc' },
+            include: {
+              sections: {
+                orderBy: { orderIndex: 'asc' },
+                include: {
+                  contentBlocks: {
+                    orderBy: { orderIndex: 'asc' },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  });
+
+  return task;
+}
+
+/**
+ * 获取用户的任务列表
+ */
+export async function getUserTasks(userId: number) {
+  return prisma.learningPath.findMany({
+    where: { userId },
+    orderBy: { createdAt: 'desc' },
+    take: 20,
+  });
+}
+
+/**
+ * 获取渐进模式的待确认状态
+ */
+export async function getPendingConfirmation(taskId: number) {
+  const task = await prisma.learningPath.findUnique({
+    where: { id: taskId },
+    include: {
+      subjects: {
+        orderBy: { orderIndex: 'asc' },
+        include: {
+          chapters: {
+            orderBy: { orderIndex: 'asc' },
+            include: {
+              sections: {
+                orderBy: { orderIndex: 'asc' },
+              },
+            },
+          },
+        },
+      },
+    },
+  });
+
+  if (!task) return null;
+
+  return {
+    id: task.id,
+    topic: task.topic,
+    status: task.status,
+    progress: task.progress,
+    currentStep: task.currentStep,
+    generationMode: task.generationMode,
+    // 待确认的学科
+    pendingSubjects: task.subjects
+      .filter(s => s.confirmationStatus === 'pending')
+      .map(s => ({ id: s.id, name: s.name, status: s.status })),
+    // 待确认的章节
+    pendingChapters: task.subjects.reduce((acc, subject) => {
+      acc[subject.id] = subject.chapters
+        .filter(c => c.confirmationStatus === 'pending')
+        .map(c => ({ id: c.id, name: c.name, status: c.status }));
+      return acc;
+    }, {} as Record<number, { id: number; name: string; status: string }[]>),
+    // 待确认的小节
+    pendingSections: task.subjects.reduce((acc, subject) => {
+      subject.chapters.forEach(chapter => {
+        acc[chapter.id] = chapter.sections
+          .filter(s => s.confirmationStatus === 'pending')
+          .map(s => ({ id: s.id, name: s.name, status: s.status }));
+      });
+      return acc;
+    }, {} as Record<number, { id: number; name: string; status: string }[]>),
+  };
+}
+
+/**
+ * 确认并继续生成
+ * @param taskId 任务ID
+ * @param step 当前步骤: subjects | chapters | sections
+ * @param confirmedIds 确认的ID列表
+ * @param modifiedItems 修改的项 [{id, name}]
+ */
+export async function confirmAndContinue(taskId: number, step: string, confirmedIds: number[], modifiedItems: { id: number; name: string }[] = []) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  // 处理修改项
+  for (const item of modifiedItems) {
+    if (step === 'subjects') {
+      await prisma.subject.update({ where: { id: item.id }, data: { name: item.name } });
+    } else if (step === 'chapters') {
+      await prisma.chapter.update({ where: { id: item.id }, data: { name: item.name } });
+    }
+  }
+
+  // 标记确认的项
+  if (step === 'subjects') {
+    await prisma.subject.updateMany({
+      where: { id: { in: confirmedIds }, learningPathId: taskId },
+      data: { confirmationStatus: 'confirmed' },
+    });
+
+    // 更新任务步骤
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: { currentStep: 'chapters' },
+    });
+
+    // 开始生成章节
+    generateChaptersStep(taskId, confirmedIds).catch(console.error);
+
+    return { currentStep: 'chapters' };
+
+  } else if (step === 'chapters') {
+    await prisma.chapter.updateMany({
+      where: { id: { in: confirmedIds } },
+      data: { confirmationStatus: 'confirmed' },
+    });
+
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: { currentStep: 'sections' },
+    });
+
+    // 开始生成小节
+    generateSectionsStep(taskId, confirmedIds).catch(console.error);
+
+    return { currentStep: 'sections' };
+
+  } else if (step === 'sections') {
+    await prisma.section.updateMany({
+      where: { id: { in: confirmedIds } },
+      data: { confirmationStatus: 'confirmed' },
+    });
+
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: { currentStep: 'content' },
+    });
+
+    // 开始生成详细内容
+    generateContentStep(taskId, confirmedIds).catch(console.error);
+
+    return { currentStep: 'content' };
+  }
+
+  return { currentStep: task.currentStep };
+}
+
+/**
+ * 重新生成某个节点
+ */
+export async function regenerateNode(taskId: number, type: 'chapter' | 'section', id: number, instruction?: string) {
+  if (type === 'chapter') {
+    const chapter = await prisma.chapter.findUnique({ where: { id } });
+    if (!chapter) throw new Error('章节不存在');
+
+    await prisma.chapter.update({ where: { id }, data: { status: 'generating', confirmationStatus: 'regenerating' } });
+
+    // 重新生成小节
+    const subject = await prisma.subject.findUnique({ where: { id: chapter.subjectId } });
+    const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+
+    const sectionsPrompt = `你是课程规划专家。请为"${chapter.name}"这一章生成学习小节列表。
+
+要求:
+1. 生成4-6个小节
+2. 每个小节应该讲解一个具体的知识点
+3. 内容要循序渐进,由浅入深
+${instruction ? `用户额外要求:${instruction}` : ''}
+
+请以JSON数组格式返回,只返回纯JSON数组`;
+
+    const sectionsResponse = await callLLM(sectionsPrompt);
+    const sectionNames = parseJsonList(sectionsResponse);
+
+    // 删除旧的小节
+    await prisma.contentBlock.deleteMany({ where: { section: { chapterId: id } } });
+    await prisma.section.deleteMany({ where: { chapterId: id } });
+
+    // 创建新的小节
+    await Promise.all(
+      sectionNames.map((name, index) =>
+        prisma.section.create({
+          data: {
+            chapterId: id,
+            name,
+            orderIndex: index,
+            status: 'pending',
+            confirmationStatus: 'pending',
+            aiResponse: sectionsResponse,
+          },
+        })
+      )
+    );
+
+    await prisma.chapter.update({ where: { id }, data: { status: 'completed', confirmationStatus: 'confirmed' } });
+
+    return { success: true, newSections: sectionNames };
+
+  } else if (type === 'section') {
+    const section = await prisma.section.findUnique({
+      where: { id },
+      include: { chapter: { include: { subject: true } } },
+    });
+    if (!section) throw new Error('小节不存在');
+
+    const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+
+    await prisma.section.update({ where: { id }, data: { status: 'generating', confirmationStatus: 'regenerating' } });
+
+    const contentPrompt = `你是专业的教学老师。请详细讲解以下知识点:
+
+知识点:${section.name}
+所属章节:${section.chapter.name}
+所属学科:${section.chapter.subject.name}
+学习主题:${task?.topic}
+${instruction ? `用户额外要求:${instruction}` : ''}
+
+要求:
+1. 用通俗易懂的语言讲解这个知识点
+2. 包含"是什么"、"为什么"、"怎么用"三个部分
+3. 可以给出具体的例子、代码、公式等
+4. 内容要详实、深入,不少于500字
+5. 直接返回正文内容,不需要标题`;
+
+    const contentResponse = await callLLM(contentPrompt);
+
+    // 删除旧内容
+    await prisma.contentBlock.deleteMany({ where: { sectionId: id } });
+
+    // 创建新内容
+    await prisma.contentBlock.create({
+      data: {
+        sectionId: id,
+        content: contentResponse,
+        orderIndex: 0,
+        wordCount: contentResponse.length,
+      },
+    });
+
+    await prisma.section.update({ where: { id }, data: { status: 'completed', confirmationStatus: 'confirmed' } });
+
+    return { success: true };
+  }
+
+  return { success: false };
+}
+
+/**
+ * 获取多Agent模式的状态
+ */
+export async function getAgentStatus(taskId: number) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) return null;
+
+  const agentStatus = task.agentStatus ? JSON.parse(task.agentStatus) : null;
+
+  return {
+    mode: task.generationMode,
+    status: task.status,
+    progress: task.progress,
+    agents: agentStatus,
+  };
+}
+
+// ============ 渐进模式函数 ============
+
+/**
+ * 渐进模式 Step 1: 生成学科列表
+ */
+async function generateSubjectsStep(taskId: number) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  console.log(`📚 渐进模式 - 生成学科列表: ${task.topic}`);
+
+  await prisma.learningPath.update({
+    where: { id: taskId },
+    data: { status: 'generating', currentStep: 'subjects' },
+  });
+
+  const subjectsPrompt = `用户想要学习主题:"${task.topic}"
+
+请作为一位专业的课程规划专家,为用户规划完整的学习路径。
+
+要求:
+1. 生成该主题下的主要学科/专业课程列表(6-10门核心学科)
+2. 每门学科应该是构建该领域知识体系所必需的
+3. 按照合理的学习顺序排列(从基础到进阶)
+
+请以JSON数组格式返回学科列表,不要加标题不要加说明,只返回纯JSON数组,例如:
+["计算机原理", "程序设计基础", "数据结构与算法", "操作系统", "计算机网络", "数据库原理", "软件工程"]`;
+
+  const subjectsResponse = await callLLM(subjectsPrompt);
+  const subjectNames = parseJsonList(subjectsResponse);
+  console.log('📚 学科列表:', subjectNames);
+
+  // 创建学科记录(带pending确认状态)
+  await Promise.all(
+    subjectNames.map((name, index) =>
+      prisma.subject.create({
+        data: {
+          learningPathId: taskId,
+          name,
+          orderIndex: index,
+          status: 'completed',
+          confirmationStatus: 'pending',
+          aiResponse: subjectsResponse,
+        },
+      })
+    )
+  );
+
+  await prisma.learningPath.update({
+    where: { id: taskId },
+    data: { progress: 10 },
+  });
+
+  console.log('📚 学科列表生成完成,等待用户确认');
+}
+
+/**
+ * 渐进模式 Step 2: 生成章节 (完全并行)
+ */
+async function generateChaptersStep(taskId: number, subjectIds: number[]) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  console.log(`📖 渐进模式 - 并行生成章节 for subjects: ${subjectIds}`);
+
+  const subjects = await prisma.subject.findMany({
+    where: { id: { in: subjectIds } },
+    orderBy: { orderIndex: 'asc' },
+  });
+
+  // 并行生成所有学科的章节
+  await Promise.all(subjects.map(async (subject, i) => {
+    console.log(`📖 生成学科章节: ${subject.name}`);
+
+    const chaptersPrompt = `你是课程规划专家。请为"${subject.name}"这门学科生成学习章节列表。
+
+要求:
+1. 生成6-10个核心章节
+2. 章节应该覆盖该学科的主要知识领域
+3. 按照合理的教学顺序排列
+
+请以JSON数组格式返回,只返回纯JSON数组`;
+
+    try {
+      const chaptersResponse = await callLLM(chaptersPrompt);
+      const chapterNames = parseJsonList(chaptersResponse);
+      console.log(`📖 ${subject.name} - 章节:`, chapterNames);
+
+      // 创建章节记录
+      await Promise.all(
+        chapterNames.map((name, index) =>
+          prisma.chapter.create({
+            data: {
+              subjectId: subject.id,
+              name,
+              orderIndex: index,
+              status: 'completed',
+              confirmationStatus: 'pending',
+              aiResponse: chaptersResponse,
+            },
+          })
+        )
+      );
+
+      await prisma.subject.update({
+        where: { id: subject.id },
+        data: { status: 'completed' },
+      });
+
+      // 更新进度
+      await prisma.learningPath.update({
+        where: { id: taskId },
+        data: { progress: 10 + Math.floor(((i + 1) / subjects.length) * 20) },
+      });
+    } catch (error) {
+      console.error(`❌ 生成章节失败 ${subject.name}:`, error);
+    }
+  }));
+
+  console.log('📖 所有章节生成完成,等待用户确认');
+}
+
+/**
+ * 渐进模式 Step 3: 生成小节 (完全并行)
+ */
+async function generateSectionsStep(taskId: number, chapterIds: number[]) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  console.log(`📑 渐进模式 - 并行生成小节 for chapters: ${chapterIds}`);
+
+  const chapters = await prisma.chapter.findMany({
+    where: { id: { in: chapterIds } },
+    orderBy: { orderIndex: 'asc' },
+    include: { subject: true },
+  });
+
+  // 并行生成所有章节的小节
+  await Promise.all(chapters.map(async (chapter, j) => {
+    console.log(`📑 生成章节小节: ${chapter.name}`);
+
+    const sectionsPrompt = `你是课程规划专家。请为"${chapter.name}"这一章生成学习小节列表。
+
+要求:
+1. 生成4-6个小节
+2. 每个小节应该讲解一个具体的知识点
+3. 内容要循序渐进,由浅入深
+
+请以JSON数组格式返回,只返回纯JSON数组`;
+
+    try {
+      const sectionsResponse = await callLLM(sectionsPrompt);
+      const sectionNames = parseJsonList(sectionsResponse);
+      console.log(`📑 ${chapter.name} - 小节:`, sectionNames);
+
+      // 创建小节记录
+      await Promise.all(
+        sectionNames.map((name, index) =>
+          prisma.section.create({
+            data: {
+              chapterId: chapter.id,
+              name,
+              orderIndex: index,
+              status: 'completed',
+              confirmationStatus: 'pending',
+              aiResponse: sectionsResponse,
+            },
+          })
+        )
+      );
+
+      await prisma.chapter.update({
+        where: { id: chapter.id },
+        data: { status: 'completed' },
+      });
+
+      // 更新进度
+      await prisma.learningPath.update({
+        where: { id: taskId },
+        data: { progress: 30 + Math.floor(((j + 1) / chapters.length) * 20) },
+      });
+    } catch (error) {
+      console.error(`❌ 生成小节失败 ${chapter.name}:`, error);
+    }
+  }));
+
+  console.log('📑 所有小节生成完成,等待用户确认');
+}
+
+/**
+ * 渐进模式 Step 4: 生成详细内容 (完全并行)
+ */
+async function generateContentStep(taskId: number, sectionIds: number[]) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  console.log(`📝 渐进模式 - 并行生成详细内容 for sections: ${sectionIds}`);
+
+  const sections = await prisma.section.findMany({
+    where: { id: { in: sectionIds } },
+    orderBy: { orderIndex: 'asc' },
+    include: { chapter: { include: { subject: true } } },
+  });
+
+  const totalSections = sections.length;
+
+  // 并行生成所有小节的详细内容
+  await Promise.all(sections.map(async (section, k) => {
+    console.log(`📝 生成详细内容: ${section.name}`);
+
+    const contentPrompt = `你是专业的教学老师。请详细讲解以下知识点:
+
+知识点:${section.name}
+所属章节:${section.chapter.name}
+所属学科:${section.chapter.subject.name}
+学习主题:${task.topic}
+
+要求:
+1. 用通俗易懂的语言讲解这个知识点
+2. 包含"是什么"、"为什么"、"怎么用"三个部分
+3. 可以给出具体的例子、代码、公式等
+4. 内容要详实、深入,不少于500字
+5. 直接返回正文内容,不需要标题`;
+
+    try {
+      const contentResponse = await callLLM(contentPrompt);
+
+      // 创建内容块
+      await prisma.contentBlock.create({
+        data: {
+          sectionId: section.id,
+          content: contentResponse,
+          orderIndex: 0,
+          wordCount: contentResponse.length,
+        },
+      });
+
+      // 更新小节状态
+      await prisma.section.update({
+        where: { id: section.id },
+        data: { status: 'completed', confirmationStatus: 'completed' },
+      });
+
+      // 更新进度 (动态计算已完成的小节数)
+      const completedSections = k + 1;
+      await prisma.learningPath.update({
+        where: { id: taskId },
+        data: { progress: 50 + Math.floor((completedSections / totalSections) * 50) },
+      });
+    } catch (error) {
+      console.error(`❌ 生成内容失败 ${section.name}:`, error);
+    }
+  }));
+
+  // 统计并完成
+  const totalBlocks = await prisma.contentBlock.count({
+    where: {
+      section: {
+        chapter: {
+          subject: { learningPathId: taskId }
+        }
+      }
+    }
+  });
+
+  await prisma.learningPath.update({
+    where: { id: taskId },
+    data: {
+      status: 'completed',
+      progress: 100,
+      currentStep: 'completed',
+      totalCount: totalBlocks,
+      completedCount: totalBlocks,
+    },
+  });
+
+  console.log(`✅ 渐进模式生成完成: ${task.topic}`);
+}
+
+// ============ 多Agent模式函数 ============
+
+/**
+ * 多Agent模式:全自动并行生成
+ */
+async function generateMultiAgent(taskId: number) {
+  const task = await prisma.learningPath.findUnique({ where: { id: taskId } });
+  if (!task) throw new Error('任务不存在');
+
+  console.log(`🚀 多Agent模式 - 开始生成学习路径: ${task.topic}`);
+
+  await prisma.learningPath.update({
+    where: { id: taskId },
+    data: {
+      status: 'generating',
+      agentStatus: JSON.stringify([
+        { name: '规划Agent', status: 'running', progress: 0 },
+      ]),
+    },
+  });
+
+  try {
+    // Step 1: 生成学科列表(规划Agent)
+    const subjectsPrompt = `用户想要学习主题:"${task.topic}"
+
+请作为一位专业的课程规划专家,为用户规划完整的学习路径。
+
+要求:
+1. 生成该主题下的主要学科/专业课程列表(6-10门核心学科)
+2. 每门学科应该是构建该领域知识体系所必需的
+3. 按照合理的学习顺序排列(从基础到进阶)
+
+请以JSON数组格式返回学科列表,不要加标题不要加说明,只返回纯JSON数组,例如:
+["计算机原理", "程序设计基础", "数据结构与算法", "操作系统", "计算机网络", "数据库原理", "软件工程"]`;
+
+    console.log('📚 规划Agent - 生成学科列表...');
+    const subjectsResponse = await callLLM(subjectsPrompt);
+    const subjectNames = parseJsonList(subjectsResponse);
+    console.log('📚 学科列表:', subjectNames);
+
+    // 创建学科记录
+    const subjects = await Promise.all(
+      subjectNames.map((name, index) =>
+        prisma.subject.create({
+          data: {
+            learningPathId: taskId,
+            name,
+            orderIndex: index,
+            status: 'pending',
+            aiResponse: subjectsResponse,
+          },
+        })
+      )
+    );
+
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: {
+        agentStatus: JSON.stringify([
+          { name: '规划Agent', status: 'completed', progress: 100 },
+          { name: '学科Agent群', status: 'running', progress: 0 },
+        ]),
+        progress: 10,
+      },
+    });
+
+    // Step 2: 并行为每个学科生成章节(学科Agent群)
+    await Promise.all(subjects.map(async (subject, i) => {
+      console.log(`📖 学科Agent[${i}] - 生成章节: ${subject.name}`);
+
+      const chaptersPrompt = `你是课程规划专家。请为"${subject.name}"这门学科生成学习章节列表。
+
+要求:
+1. 生成6-10个核心章节
+2. 章节应该覆盖该学科的主要知识领域
+3. 按照合理的教学顺序排列
+
+请以JSON数组格式返回,只返回纯JSON数组`;
+
+      const chaptersResponse = await callLLM(chaptersPrompt);
+      const chapterNames = parseJsonList(chaptersResponse);
+
+      const chapters = await Promise.all(
+        chapterNames.map((name, index) =>
+          prisma.chapter.create({
+            data: {
+              subjectId: subject.id,
+              name,
+              orderIndex: index,
+              status: 'pending',
+              aiResponse: chaptersResponse,
+            },
+          })
+        )
+      );
+
+      await prisma.subject.update({
+        where: { id: subject.id },
+        data: { status: 'generating' },
+      });
+
+      // Step 3: 并行为每个章节生成小节
+      await Promise.all(chapters.map(async (chapter) => {
+        const sectionsPrompt = `你是课程规划专家。请为"${chapter.name}"这一章生成学习小节列表。
+
+要求:
+1. 生成4-6个小节
+2. 每个小节应该讲解一个具体的知识点
+3. 内容要循序渐进,由浅入深
+
+请以JSON数组格式返回,只返回纯JSON数组`;
+
+        const sectionsResponse = await callLLM(sectionsPrompt);
+        const sectionNames = parseJsonList(sectionsResponse);
+
+        const sections = await Promise.all(
+          sectionNames.map((name, index) =>
+            prisma.section.create({
+              data: {
+                chapterId: chapter.id,
+                name,
+                orderIndex: index,
+                status: 'pending',
+                aiResponse: sectionsResponse,
+              },
+            })
+          )
+        );
+
+        await prisma.chapter.update({
+          where: { id: chapter.id },
+          data: { status: 'completed' },
+        });
+
+        // Step 4: 并行为每个小节生成详细内容
+        await Promise.all(sections.map(async (section) => {
+          const contentPrompt = `你是专业的教学老师。请详细讲解以下知识点:
+
+知识点:${section.name}
+所属章节:${chapter.name}
+所属学科:${subject.name}
+学习主题:${task.topic}
+
+要求:
+1. 用通俗易懂的语言讲解这个知识点
+2. 包含"是什么"、"为什么"、"怎么用"三个部分
+3. 可以给出具体的例子、代码、公式等
+4. 内容要详实、深入,不少于500字
+5. 直接返回正文内容,不需要标题`;
+
+          try {
+            const contentResponse = await callLLM(contentPrompt);
+
+            await prisma.contentBlock.create({
+              data: {
+                sectionId: section.id,
+                content: contentResponse,
+                orderIndex: 0,
+                wordCount: contentResponse.length,
+              },
+            });
+
+            await prisma.section.update({
+              where: { id: section.id },
+              data: { status: 'completed' },
+            });
+          } catch (error) {
+            console.error(`❌ 生成内容失败 ${section.name}:`, error);
+          }
+        }));
+      }));
+
+      await prisma.subject.update({
+        where: { id: subject.id },
+        data: { status: 'completed' },
+      });
+
+      // 更新学科Agent进度
+      const subjectProgress = Math.floor(((i + 1) / subjects.length) * 40) + 10;
+      await prisma.learningPath.update({
+        where: { id: taskId },
+        data: {
+          progress: subjectProgress,
+          agentStatus: JSON.stringify([
+            { name: '规划Agent', status: 'completed', progress: 100 },
+            { name: '学科Agent群', status: 'running', progress: subjectProgress },
+          ]),
+        },
+      });
+    }));
+
+    // 统计并完成
+    const totalBlocks = await prisma.contentBlock.count({
+      where: {
+        section: {
+          chapter: {
+            subject: { learningPathId: taskId }
+          }
+        }
+      }
+    });
+
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: {
+        status: 'completed',
+        progress: 100,
+        totalCount: totalBlocks,
+        completedCount: totalBlocks,
+        agentStatus: JSON.stringify([
+          { name: '规划Agent', status: 'completed', progress: 100 },
+          { name: '学科Agent群', status: 'completed', progress: 100 },
+          { name: '整合Agent', status: 'completed', progress: 100 },
+        ]),
+      },
+    });
+
+    console.log(`✅ 多Agent模式生成完成: ${task.topic}`);
+
+  } catch (error: any) {
+    console.error('❌ 多Agent生成失败:', error);
+    await prisma.learningPath.update({
+      where: { id: taskId },
+      data: { status: 'failed', errorMsg: error.message },
+    });
+    throw error;
+  }
+}

+ 18 - 9
server/src/modules/ai/ai.controller.ts

@@ -5,13 +5,22 @@ import { config } from '../../config';
 
 
 const router = new Router();
 const router = new Router();
 
 
-// 免费优先的模型列表
-const FREE_MODELS = [
-  'tongyi-xiaomi-analysis-pro',
-  'qwen3.5-122b-a10b',
+// 可用模型列表(已测试可用)
+const AVAILABLE_MODELS = [
   'qwen-plus',
   'qwen-plus',
+  'qwen-max',
+  'qwen-turbo',
+  'MiniMax-M2.5',
+  'tongyi-xiaomi-analysis-pro',
+  'tongyi-xiaomi-analysis-flash',
+  'MiniMax-M2.1',
 ];
 ];
 
 
+// 随机选择模型
+function getRandomModel(): string {
+  return AVAILABLE_MODELS[Math.floor(Math.random() * AVAILABLE_MODELS.length)];
+}
+
 // AI 生成文本
 // AI 生成文本
 router.post('/generate', async (ctx: Context) => {
 router.post('/generate', async (ctx: Context) => {
   const { prompt, model } = ctx.request.body as {
   const { prompt, model } = ctx.request.body as {
@@ -35,8 +44,8 @@ router.post('/generate', async (ctx: Context) => {
       return;
       return;
     }
     }
 
 
-    // 优先使用指定的模型,否则使用免费的模型列表
-    const selectedModel = model || FREE_MODELS[0];
+    // 优先使用指定的模型,否则随机选择
+    const selectedModel = model || getRandomModel();
     
     
     console.log(`🤖 使用模型: ${selectedModel}`);
     console.log(`🤖 使用模型: ${selectedModel}`);
 
 
@@ -57,7 +66,7 @@ router.post('/generate', async (ctx: Context) => {
           'Authorization': `Bearer ${apiKey}`,
           'Authorization': `Bearer ${apiKey}`,
           'Content-Type': 'application/json',
           'Content-Type': 'application/json',
         },
         },
-        timeout: 60000,
+        timeout: 120000,
       }
       }
     );
     );
 
 
@@ -95,8 +104,8 @@ router.get('/models', async (ctx: Context) => {
     code: 0,
     code: 0,
     message: 'success',
     message: 'success',
     data: {
     data: {
-      models: FREE_MODELS,
-      default: FREE_MODELS[0],
+      models: AVAILABLE_MODELS,
+      default: 'qwen-plus',
     },
     },
   };
   };
 });
 });