|
|
@@ -44,6 +44,74 @@ export class AIContentService {
|
|
|
this.model = DEFAULT_MODEL;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 调用 DashScope API (流式)
|
|
|
+ */
|
|
|
+ private async *streamLLM(prompt: string, systemPrompt?: string): AsyncGenerator<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',
|
|
|
+ stream: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${this.apiKey}`,
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ timeout: 180000,
|
|
|
+ responseType: 'stream',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ let buffer = '';
|
|
|
+ for await (const chunk of response.data) {
|
|
|
+ buffer += chunk.toString();
|
|
|
+
|
|
|
+ // 解析SSE格式的数据
|
|
|
+ const lines = buffer.split('\n');
|
|
|
+ buffer = lines.pop() || '';
|
|
|
+
|
|
|
+ for (const line of lines) {
|
|
|
+ if (line.startsWith('data:')) {
|
|
|
+ const data = line.slice(5).trim();
|
|
|
+ if (data && data !== '[DONE]') {
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(data);
|
|
|
+ const content = parsed.output?.choices?.[0]?.message?.content;
|
|
|
+ if (content) {
|
|
|
+ yield content;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略解析错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ LLM 流式调用失败:', error.response?.data || error.message);
|
|
|
+ throw new Error(error.message || 'AI 生成失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 调用 DashScope API
|
|
|
*/
|
|
|
@@ -344,6 +412,83 @@ ${chapterTitle ? `章节标题:${chapterTitle}` : ''}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 流式生成章节内容 - 使用 LLM SSE
|
|
|
+ */
|
|
|
+ async *streamGenerateChunk(outlineId: string, chapterIndex: number, chapterTitle?: string, previousContent?: string) {
|
|
|
+ const prompt = `请续写以下小说内容:
|
|
|
+
|
|
|
+${previousContent ? `前文内容:\n${previousContent}\n\n` : ''}
|
|
|
+请续写第${chapterIndex + 1}章内容。
|
|
|
+
|
|
|
+${chapterTitle ? `章节标题:${chapterTitle}` : ''}
|
|
|
+
|
|
|
+要求:
|
|
|
+1. 内容要丰富、生动,不少于2000字
|
|
|
+2. 包含人物对话、心理描写、场景描写
|
|
|
+3. 情节要紧凑,有吸引力
|
|
|
+4. 直接返回正文内容,不需要额外说明`;
|
|
|
+
|
|
|
+ const chunkId = `chunk-${Date.now()}-${chapterIndex}`;
|
|
|
+ let fullContent = '';
|
|
|
+ let charCount = 0;
|
|
|
+
|
|
|
+ // 先发送开始信号
|
|
|
+ yield {
|
|
|
+ type: 'start',
|
|
|
+ chunkId,
|
|
|
+ chapterIndex,
|
|
|
+ message: '开始生成...',
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ for await (const chunk of this.streamLLM(prompt)) {
|
|
|
+ fullContent += chunk;
|
|
|
+ charCount += chunk.length;
|
|
|
+
|
|
|
+ // 实时发送内容片段
|
|
|
+ yield {
|
|
|
+ type: 'content',
|
|
|
+ chunkId,
|
|
|
+ chapterIndex,
|
|
|
+ content: chunk,
|
|
|
+ charCount,
|
|
|
+ message: `已生成 ${charCount} 字...`,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送完成信号
|
|
|
+ yield {
|
|
|
+ type: 'done',
|
|
|
+ chunkId,
|
|
|
+ chapterIndex,
|
|
|
+ content: fullContent,
|
|
|
+ charCount: fullContent.length,
|
|
|
+ wordCount: this.estimateWordCount(fullContent),
|
|
|
+ status: 'completed',
|
|
|
+ message: '生成完成!',
|
|
|
+ };
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('流式生成失败:', error);
|
|
|
+ yield {
|
|
|
+ type: 'error',
|
|
|
+ chunkId,
|
|
|
+ chapterIndex,
|
|
|
+ message: error.message || '生成失败',
|
|
|
+ status: 'error',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 估算字数(中文按字符,英文按单词)
|
|
|
+ */
|
|
|
+ private estimateWordCount(text: string): number {
|
|
|
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
|
+ const englishWords = (text.match(/[a-zA-Z]+/g) || []).length;
|
|
|
+ return chineseChars + Math.floor(englishWords * 0.5);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 流式生成(模拟SSE)
|
|
|
*/
|