From 3d0d16a3e597fc58fb75bd642fd0a3aad16997cb Mon Sep 17 00:00:00 2001 From: empty Date: Thu, 8 Jan 2026 10:54:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E8=BE=93=E5=87=BA=E4=B8=8E=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 DeepSeekAPI.js,实现稳健的 SSE 流式解析 - 将核心业务逻辑(生成、分析)移入 appStore.js - 优化 WriterPanel 和 AnalysisPanel 组件,移除冗余逻辑 - 更新文档,补充架构演进说明 --- README.md | 15 +++ src/api/deepseek.js | 166 +++++++++++++++++++------------ src/components/AnalysisPanel.vue | 75 +------------- src/components/WriterPanel.vue | 57 +---------- src/stores/app.js | 96 +++++++++++++++++- 5 files changed, 217 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 3d81a88..6ca2008 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ src/ └── main.js # 入口文件 ``` +## 架构演进与优化 + +### 1. 流式输出增强 (Stream Optimization) +项目在 `DeepSeekAPI.js` 中实现了稳健的 SSE (Server-Sent Events) 解析器: +- **逐行扫描**:放弃简单的 `split` 分割,改用逐字符缓冲区扫描,确保处理被网络分包截断的 JSON 数据。 +- **协议兼容**:完美支持标准 SSE 协议,兼容 data: 后有无空格的各种情况。 +- **状态管理**:通过 `appStore` 统一管理生成状态,实现组件间的数据实时同步,保证 UI 渲染无延迟。 + +### 2. 状态管理重构 +- **逻辑收拢**:将所有 API 调用和业务逻辑(生成、分析)移入 Pinia Store (`appStore.js`)。 +- **组件纯粹化**:`WriterPanel` 和 `AnalysisPanel` 仅负责 UI 渲染和用户交互,不再包含核心业务逻辑。 + +### 3. 深度集成 +- **应用到写作**:实现了从"范式分析"到"写作工坊"的深度数据迁移,包括原文引用、风格约束注入和范式模板自动匹配。 + ## 功能特性 ### 1. AI 写作工坊 diff --git a/src/api/deepseek.js b/src/api/deepseek.js index dc8060b..8cec1a0 100644 --- a/src/api/deepseek.js +++ b/src/api/deepseek.js @@ -1,79 +1,115 @@ -import axios from 'axios' - class DeepSeekAPI { constructor(config) { this.baseURL = config.url this.apiKey = config.key - this.client = axios.create({ - baseURL: this.baseURL, - timeout: 60000, - headers: { - 'Content-Type': 'application/json' + console.log('DeepSeekAPI 已初始化:', { baseURL: this.baseURL }) + } + + async _streamRequest(messages, options = {}, onContent) { + const authHeader = this.apiKey.trim().startsWith('Bearer ') + ? this.apiKey.trim() + : `Bearer ${this.apiKey.trim()}`; + + console.log('DeepSeekAPI: Starting stream request...', { messagesLength: messages.length }) + + try { + const response = await fetch(this.baseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHeader + }, + body: JSON.stringify({ + model: 'deepseek-chat', + messages, + stream: true, + ...options + }) + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('DeepSeekAPI: HTTP Error', response.status, errorText) + throw new Error(`API请求失败: ${response.status} ${errorText}`) } - }) + + console.log('DeepSeekAPI: Stream connection established.') + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) { + console.log('DeepSeekAPI: Stream finished by server.') + break + } + + buffer += decoder.decode(value, { stream: true }) + + // Split by newline + let lines = buffer.split('\n') + // Keep the last part (potential incomplete line) in buffer + buffer = lines.pop() || '' + + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine) continue + + if (trimmedLine.startsWith('data:')) { + const data = trimmedLine.substring(5).trim() // Remove 'data:' prefix + + if (data === '[DONE]') { + console.log('DeepSeekAPI: Received [DONE] signal.') + return + } + + try { + const parsed = JSON.parse(data) + const content = parsed.choices?.[0]?.delta?.content || '' + if (content) { + // console.log('DeepSeekAPI: Received content chunk:', content.length) // Too verbose for large text + if (onContent) { + onContent(content) + } + } + } catch (e) { + console.warn('DeepSeekAPI: JSON parse error for line:', trimmedLine, e) + } + } + } + } + } catch (err) { + console.error('DeepSeekAPI: Stream processing error:', err); + throw err; + } } - async generateContent(prompt, options = {}) { - const response = await fetch(this.baseURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}` + async generateContent(prompt, onContent, options = {}) { + return this._streamRequest([ + { + role: 'system', + content: '你是一个资深的专业写作助手,请严格按照用户的要求进行创作。' }, - body: JSON.stringify({ - model: 'deepseek-chat', - messages: [ - { - role: 'system', - content: '你是一个资深的专业写作助手,请严格按照用户的要求进行创作。' - }, - { - role: 'user', - content: prompt - } - ], - stream: true, - temperature: 0.7, - ...options - }) - }) - - if (!response.ok) { - throw new Error(`API请求失败: ${response.status}`) - } - - return response + { + role: 'user', + content: prompt + } + ], { temperature: 0.7, ...options }, onContent) } - async analyzeContent(text) { - const response = await fetch(this.baseURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}` + async analyzeContent(text, onContent) { + return this._streamRequest([ + { + role: 'system', + content: '你是一个专业的写作分析师,擅长分析文章的写作范式、结构和特点。请从以下几个方面分析文章:1. 主要写作范式类型 2. 文章结构特点 3. 语言风格特征 4. 目标读者群体 5. 写作技巧总结。请用简洁明了的语言回答,使用Markdown格式。' }, - body: JSON.stringify({ - model: 'deepseek-chat', - messages: [ - { - role: 'system', - content: '你是一个专业的写作分析师,擅长分析文章的写作范式、结构和特点。请从以下几个方面分析文章:1. 主要写作范式类型 2. 文章结构特点 3. 语言风格特征 4. 目标读者群体 5. 写作技巧总结。请用简洁明了的语言回答,使用Markdown格式。' - }, - { - role: 'user', - content: `请分析以下文章的写作范式:\n\n${text}` - } - ], - stream: true, - temperature: 0.3 - }) - }) - - if (!response.ok) { - throw new Error(`API请求失败: ${response.status}`) - } - - return response + { + role: 'user', + content: `请分析以下文章的写作范式:\n\n${text}` + } + ], { temperature: 0.3 }, onContent) } } diff --git a/src/components/AnalysisPanel.vue b/src/components/AnalysisPanel.vue index 9f8f35b..1344c7d 100644 --- a/src/components/AnalysisPanel.vue +++ b/src/components/AnalysisPanel.vue @@ -203,81 +203,12 @@ const applyParadigm = (paradigm) => { // 分析文章 const analyzeArticle = async () => { - if (!analysisText.value.trim()) { - alert('请输入要分析的文章内容') - return - } - - isAnalyzing.value = true - appStore.analysisResult = { - paradigm: '分析中...', - paradigmType: null, - analysis: '', - timestamp: new Date() - } - try { - const api = new DeepSeekAPI({ - url: appStore.apiUrl, - key: appStore.apiKey - }) - - const response = await api.analyzeContent(analysisText.value) - const reader = response.body.getReader() - const decoder = new TextDecoder() - - let fullContent = '' - - while (true) { - const { done, value } = await reader.read() - if (done) break - - const chunk = decoder.decode(value) - const lines = chunk.split('\n').filter(line => line.trim()) - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6) - if (data === '[DONE]') continue - - try { - const parsed = JSON.parse(data) - const content = parsed.choices?.[0]?.delta?.content || '' - if (content) { - fullContent += content - // 实时更新分析结果 - appStore.analysisResult = { - paradigm: '分析中...', - paradigmType: null, - analysis: fullContent, - timestamp: new Date() - } - } - } catch (e) { - // 忽略解析错误 - } - } - } - } - - // 分析完成后检测范式类型 - const detectedParadigm = detectParadigm(fullContent) - appStore.analysisResult = { - paradigm: detectedParadigm.name, - paradigmType: detectedParadigm.type, - analysis: fullContent, - timestamp: new Date() - } - + const result = await appStore.analyzeArticleAction(analysisText.value, detectParadigm) // 添加到历史记录 - addToHistory(detectedParadigm.name, analysisText.value, fullContent) + addToHistory(result.paradigm, analysisText.value, result.content) } catch (error) { - appStore.analysisResult = { - error: true, - message: error.message - } - } finally { - isAnalyzing.value = false + alert(error.message) } } diff --git a/src/components/WriterPanel.vue b/src/components/WriterPanel.vue index f73f83a..df4274d 100644 --- a/src/components/WriterPanel.vue +++ b/src/components/WriterPanel.vue @@ -130,9 +130,6 @@ diff --git a/src/stores/app.js b/src/stores/app.js index b9c0ad4..a1136a7 100644 --- a/src/stores/app.js +++ b/src/stores/app.js @@ -1,6 +1,8 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { config } from '../utils/config.js' +import DeepSeekAPI from '../api/deepseek.js' +import { buildPrompt } from '../utils/promptBuilder.js' export const useAppStore = defineStore('app', () => { // 页面状态 @@ -42,6 +44,96 @@ export const useAppStore = defineStore('app', () => { }) } + // 生成内容动作 + const generateContentAction = async () => { + if (!apiUrl.value || !apiKey.value || apiKey.value === 'YOUR_KEY') { + throw new Error('请先配置 API 地址和 API Key') + } + + console.log('Store: generateContentAction 启动') + isGenerating.value = true + generatedContent.value = '' + + try { + const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value }) + const prompt = buildPrompt( + inputTask.value, + [...selectedTags.value, customConstraint.value].filter(Boolean), + references.value + ) + + console.log('Store: 调用 API 生成内容...') + await api.generateContent(prompt, (content) => { + // console.log('Store: 收到生成内容块', content.length) // Verbose + generatedContent.value += content + }) + console.log('Store: 生成内容完成') + } catch (error) { + console.error('Store: 生成内容失败:', error) + throw error + } finally { + isGenerating.value = false + console.log('Store: isGenerating 重置为 false') + } + } + + // 分析文章动作 + const analyzeArticleAction = async (text, detectParadigmFn) => { + if (!text.trim()) { + throw new Error('请输入要分析的文章内容') + } + + console.log('Store: analyzeArticleAction 启动') + isAnalyzing.value = true + analysisResult.value = { + paradigm: '分析中...', + paradigmType: null, + analysis: '', + timestamp: new Date() + } + + try { + const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value }) + let fullContent = '' + + console.log('Store: 调用 API 分析文章...') + await api.analyzeContent(text, (content) => { + fullContent += content + // 实时更新分析结果,用于 UI 展示流式效果 + analysisResult.value = { + paradigm: '分析中...', + paradigmType: null, + analysis: fullContent, + timestamp: new Date() + } + }) + console.log('Store: 分析文章流式接收完成') + + // 分析完成后检测范式类型 + const detectedParadigm = detectParadigmFn(fullContent) + console.log('Store: 检测到范式类型:', detectedParadigm.name) + + analysisResult.value = { + paradigm: detectedParadigm.name, + paradigmType: detectedParadigm.type, + analysis: fullContent, + timestamp: new Date() + } + + return { paradigm: detectedParadigm.name, content: fullContent } + } catch (error) { + console.error('Store: 分析失败:', error) + analysisResult.value = { + error: true, + message: error.message + } + throw error + } finally { + isAnalyzing.value = false + console.log('Store: isAnalyzing 重置为 false') + } + } + // 切换页面 const switchPage = (page) => { currentPage.value = page @@ -69,6 +161,8 @@ export const useAppStore = defineStore('app', () => { // 方法 switchPage, - addReferenceFromAnalysis + addReferenceFromAnalysis, + generateContentAction, + analyzeArticleAction } })