|
|
@@ -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();
|