فهرست منبع

feat(ai-content): 接入DashScope LLM API 实现真实内容生成

- generateOutline: 使用LLM生成真实小说大纲
- generateCharacters: 使用LLM生成角色设定
- generateChunk: 使用LLM生成完整章节内容
- optimizeContent: 使用LLM优化内容
- adjustEmotion: 使用LLM调节情感
- generateDialogue: 使用LLM生成对话
- optimizeSEO: 使用LLM生成SEO优化建议
- continueContent: 使用LLM智能续写

fallback: 接口失败时返回默认数据

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MyFramework User 5 ماه پیش
والد
کامیت
4c6de24064
2فایلهای تغییر یافته به همراه315 افزوده شده و 49 حذف شده
  1. 7 2
      server/src/modules/ai-content/ai-content.controller.ts
  2. 308 47
      server/src/modules/ai-content/ai-content.service.ts

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

@@ -62,8 +62,13 @@ router.post('/characters/generate', async (ctx) => {
 
 // 分步生成内容
 router.post('/chunk/generate', async (ctx) => {
-  const { outlineId, chapterIndex } = ctx.request.body as { outlineId: string; chapterIndex: number };
-  const result = await aiContentService.generateChunk(outlineId, chapterIndex);
+  const { outlineId, chapterIndex, chapterTitle, previousContent } = ctx.request.body as {
+    outlineId: string;
+    chapterIndex: number;
+    chapterTitle?: string;
+    previousContent?: string;
+  };
+  const result = await aiContentService.generateChunk(outlineId, chapterIndex, chapterTitle, previousContent);
   ctx.body = { code: 0, message: 'success', data: result };
 });
 

+ 308 - 47
server/src/modules/ai-content/ai-content.service.ts

@@ -1,8 +1,11 @@
 /**
  * AI内容生成服务
- * 模拟LLM响应,实际生产环境应接入通义千问/DashScope API
+ * 使用通义千问/DashScope API
  */
 
+import axios from 'axios';
+import { config } from '../../config';
+
 // 内容类型分类
 const contentTypes = {
   '创作类': ['小说', '故事', '剧本', '诗歌', '散文'],
@@ -29,12 +32,69 @@ const languages = ['中文', '英语', '日语', '韩语', '法语', '德语', '
 // 质量评分维度
 const qualityDimensions = ['fluency', 'naturalness', 'emotion_consistency', 'topic_adherence', 'structural_integrity'];
 
+// 默认模型
+const DEFAULT_MODEL = 'qwen-plus';
+
 export class AIContentService {
+  private apiKey: string;
+  private model: string;
+
+  constructor() {
+    this.apiKey = config.dashscope.apiKey || '';
+    this.model = DEFAULT_MODEL;
+  }
+
+  /**
+   * 调用 DashScope API
+   */
+  private async callLLM(prompt: string, systemPrompt?: string): Promise<string> {
+    if (!this.apiKey) {
+      throw new Error('未配置 AI API Key');
+    }
+
+    const messages: any[] = [];
+    if (systemPrompt) {
+      messages.push({ role: 'system', content: systemPrompt });
+    }
+    messages.push({ role: 'user', content: prompt });
+
+    try {
+      const response = await axios.post(
+        'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
+        {
+          model: this.model,
+          input: {
+            messages,
+          },
+          parameters: {
+            result_format: 'message',
+          },
+        },
+        {
+          headers: {
+            'Authorization': `Bearer ${this.apiKey}`,
+            'Content-Type': 'application/json',
+          },
+          timeout: 120000,
+        }
+      );
+
+      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.response?.data || error.message);
+      throw new Error(error.message || 'AI 生成失败');
+    }
+  }
+
   /**
    * 智能意图识别
    */
   async recognizeIntent(input: string) {
-    // 模拟意图识别
     const type = this.detectContentType(input);
     const industry = this.detectIndustry(input);
 
@@ -43,7 +103,7 @@ export class AIContentService {
       industry,
       style: '正式',
       scale: input.length > 500 ? '长篇' : '短篇',
-      confidence: 0.85 + Math.random() * 0.1,
+      confidence: 0.85,
     };
   }
 
@@ -154,50 +214,141 @@ export class AIContentService {
   }
 
   /**
-   * 生成大纲
+   * 生成大纲 - 使用 LLM
    */
   async generateOutline(type: string, theme: string, chapters: number) {
-    const outline = [];
-    for (let i = 1; i <= chapters; i++) {
-      outline.push({
-        id: `chapter-${i}`,
-        title: `第${i}章:${theme}的展开`,
-        description: `详细描述第${i}章的情节发展`,
-        wordCount: 3000 + Math.floor(Math.random() * 2000),
-      });
+    const prompt = `请为一部${type}生成大纲。
+
+主题:${theme}
+章节数:${chapters}章
+
+请以JSON格式返回,格式如下:
+{
+  "outline": [
+    {"id": "chapter-1", "title": "第1章:xxx", "description": "本章情节描述", "wordCount": xxx},
+    ...
+  ]
+}
+
+要求:
+1. 每章标题要体现本章核心情节
+2. 描述要详细说明本章发生的关键事件
+3. 每章预估字数3000-5000字
+4. 章节之间要有逻辑衔接`;
+
+    try {
+      const response = await this.callLLM(prompt);
+      // 尝试解析JSON
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const parsed = JSON.parse(jsonMatch[0]);
+        return {
+          outlineId: `outline-${Date.now()}`,
+          outline: parsed.outline || [],
+          theme,
+        };
+      }
+      // 如果无法解析JSON,返回模拟数据
+      throw new Error('无法解析LLM响应');
+    } catch (error) {
+      console.log('大纲生成失败,使用默认大纲:', error);
+      // 返回默认大纲
+      const outline = [];
+      for (let i = 1; i <= chapters; i++) {
+        outline.push({
+          id: `chapter-${i}`,
+          title: `第${i}章:${theme}的展开`,
+          description: `详细描述第${i}章的情节发展,包括人物互动和故事推进`,
+          wordCount: 3500 + Math.floor(Math.random() * 1500),
+        });
+      }
+      return { outlineId: `outline-${Date.now()}`, outline, theme };
     }
-    return { outlineId: `outline-${Date.now()}`, outline, theme };
   }
 
   /**
-   * 生成角色设定
+   * 生成角色设定 - 使用 LLM
    */
   async generateCharacters(type: string, genre: string) {
-    const characters = [
-      { id: 'char-1', name: '张三', age: 30, gender: '男', personality: '正直勇敢', role: '主角', avatar: '' },
-      { id: 'char-2', name: '李四', age: 28, gender: '女', personality: '聪明机智', role: '女主', avatar: '' },
-    ];
-    return { characters };
+    const prompt = `为一个${type}项目生成角色设定。
+
+题材风格:${genre}
+
+请生成2-4个主要角色,以JSON格式返回:
+{
+  "characters": [
+    {"id": "char-1", "name": "角色名", "age": 年龄, "gender": "男/女", "personality": "性格特点", "role": "主角/配角", "avatar": ""},
+    ...
+  ]
+}
+
+要求:
+1. 主角性格要鲜明,有成长空间
+2. 配角要有独特个性
+3. 人物关系要合理`;
+
+    try {
+      const response = await this.callLLM(prompt);
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const parsed = JSON.parse(jsonMatch[0]);
+        return parsed;
+      }
+      throw new Error('无法解析LLM响应');
+    } catch (error) {
+      console.log('角色生成失败,使用默认角色:', error);
+      return {
+        characters: [
+          { id: 'char-1', name: '林浩', age: 28, gender: '男', personality: '正直勇敢,有责任心', role: '主角', avatar: '' },
+          { id: 'char-2', name: '苏晴', age: 26, gender: '女', personality: '聪明机智,温柔体贴', role: '女主', avatar: '' },
+        ],
+      };
+    }
   }
 
   /**
-   * 分步生成内容
+   * 分步生成内容 - 使用 LLM
    */
-  async generateChunk(outlineId: string, chapterIndex: number) {
-    return {
-      chunkId: `chunk-${Date.now()}-${chapterIndex}`,
-      chapterIndex,
-      content: `这是第${chapterIndex + 1}章的内容...\n\n[模拟生成的长文本内容,包含故事情节、人物对话等丰富元素。]`,
-      wordCount: 3500 + Math.floor(Math.random() * 1500),
-      status: 'completed',
-    };
+  async generateChunk(outlineId: string, chapterIndex: number, chapterTitle?: string, previousContent?: string) {
+    const prompt = `请续写以下小说内容:
+
+${previousContent ? `前文内容:\n${previousContent}\n\n` : ''}
+请续写第${chapterIndex + 1}章内容。
+
+${chapterTitle ? `章节标题:${chapterTitle}` : ''}
+
+要求:
+1. 内容要丰富、生动,不少于2000字
+2. 包含人物对话、心理描写、场景描写
+3. 情节要紧凑,有吸引力
+4. 直接返回正文内容,不需要额外说明`;
+
+    try {
+      const content = await this.callLLM(prompt);
+      return {
+        chunkId: `chunk-${Date.now()}-${chapterIndex}`,
+        chapterIndex,
+        content: content,
+        wordCount: content.length,
+        status: 'completed',
+      };
+    } catch (error) {
+      console.log('内容生成失败:', error);
+      return {
+        chunkId: `chunk-${Date.now()}-${chapterIndex}`,
+        chapterIndex,
+        content: `第${chapterIndex + 1}章内容\n\n[AI生成内容因接口问题暂未返回,请稍后重试...]`,
+        wordCount: 0,
+        status: 'error',
+      };
+    }
   }
 
   /**
    * 流式生成(模拟SSE)
    */
   async *streamGenerate(outlineId: string) {
-    const chunks = ['第一章的内容开始...', '情节发展...', '高潮迭起...', '最终结局...'];
+    const chunks = ['内容开始...', '情节发展...', '高潮迭起...', '最终结局...'];
 
     for (const chunk of chunks) {
       await new Promise(resolve => setTimeout(resolve, 500));
@@ -286,11 +437,30 @@ export class AIContentService {
    * 内容优化
    */
   async optimizeContent(text: string, target: string) {
-    return {
-      original: text,
-      optimized: `[优化后] ${text}`,
-      improvements: ['语言更生动', '结构更清晰'],
-    };
+    const prompt = `请优化以下内容,使其${target}:
+
+原文:
+${text}
+
+要求:
+1. 保持原文核心意思
+2. 语言更加生动、流畅
+3. 直接返回优化后的内容`;
+
+    try {
+      const optimized = await this.callLLM(prompt);
+      return {
+        original: text,
+        optimized: optimized,
+        improvements: ['语言更生动', '结构更清晰'],
+      };
+    } catch (error) {
+      return {
+        original: text,
+        optimized: `[优化后] ${text}`,
+        improvements: ['语言更生动', '结构更清晰'],
+      };
+    }
   }
 
   /**
@@ -338,12 +508,32 @@ export class AIContentService {
    * 情感调节
    */
   async adjustEmotion(text: string, targetEmotion: string) {
-    return {
-      original: text,
-      adjusted: `[${targetEmotion}风格] ${text}`,
-      emotion: targetEmotion,
-      intensity: 0.8,
-    };
+    const prompt = `请将以下内容的情感调整为${targetEmotion}风格:
+
+原文:
+${text}
+
+要求:
+1. 保持原文核心意思
+2. 情感表达更加${targetEmotion}
+3. 直接返回调整后的内容`;
+
+    try {
+      const adjusted = await this.callLLM(prompt);
+      return {
+        original: text,
+        adjusted: adjusted,
+        emotion: targetEmotion,
+        intensity: 0.8,
+      };
+    } catch (error) {
+      return {
+        original: text,
+        adjusted: `[${targetEmotion}风格] ${text}`,
+        emotion: targetEmotion,
+        intensity: 0.8,
+      };
+    }
   }
 
   /**
@@ -357,18 +547,63 @@ export class AIContentService {
    * 多角色对话生成
    */
   async generateDialogue(characters: Array<{ name: string; voice: string }>, scenario: string) {
-    const lines = characters.map((char, i) => ({
-      character: char.name,
-      voice: char.voice,
-      dialogue: `这是${char.name}的对话内容...`,
-    }));
-    return { lines, scenario };
+    const charactersDesc = characters.map(c => `${c.name}(音色:${c.voice})`).join('、');
+    const prompt = `请为以下角色生成一段对话:
+
+角色:${charactersDesc}
+场景:${scenario}
+
+要求:
+1. 对话自然流畅,符合各角色性格
+2. 推动情节发展
+3. 直接返回对话内容`;
+
+    try {
+      const dialogue = await this.callLLM(prompt);
+      const lines = dialogue.split('\n').filter(line => line.trim());
+      return {
+        lines: lines.map((line, i) => ({
+          character: characters[i % characters.length]?.name || '未知',
+          voice: characters[i % characters.length]?.voice || '',
+          dialogue: line,
+        })),
+        scenario,
+      };
+    } catch (error) {
+      const lines = characters.map((char, i) => ({
+        character: char.name,
+        voice: char.voice,
+        dialogue: `这是${char.name}的对话内容...`,
+      }));
+      return { lines, scenario };
+    }
   }
 
   /**
    * SEO优化
    */
   async optimizeSEO(title: string, content: string, platform: string) {
+    const prompt = `请为以下内容进行SEO优化:
+
+标题:${title}
+内容:${content.slice(0, 500)}...
+目标平台:${platform}
+
+请以JSON格式返回:
+{
+  "optimizedTitle": "优化后的标题",
+  "keywords": ["关键词1", "关键词2", "关键词3"],
+  "suggestions": ["优化建议1", "优化建议2"]
+}`;
+
+    try {
+      const response = await this.callLLM(prompt);
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        return JSON.parse(jsonMatch[0]);
+      }
+    } catch (error) {}
+
     return {
       optimizedTitle: `[SEO优化] ${title}`,
       keywords: ['关键词1', '关键词2', '关键词3'],
@@ -405,6 +640,32 @@ export class AIContentService {
    * 智能续写
    */
   async continueContent(text: string, direction: string) {
+    const prompt = `请续写以下内容,方向:${direction}:
+
+原文:
+${text}
+
+要求:
+1. 保持原文风格
+2. 情节自然发展
+3. 提供2-3个不同的续写方向
+4. 以JSON格式返回:
+{
+  "continuations": [
+    {"content": "续写方向1", "score": 0.9},
+    {"content": "续写方向2", "score": 0.8}
+  ],
+  "selected": 0
+}`;
+
+    try {
+      const response = await this.callLLM(prompt);
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        return JSON.parse(jsonMatch[0]);
+      }
+    } catch (error) {}
+
     return {
       continuations: [
         { content: `续写方向1: ${text}...`, score: 0.9 },
@@ -415,4 +676,4 @@ export class AIContentService {
   }
 }
 
-export const aiContentService = new AIContentService();
+export const aiContentService = new AIContentService();