feat: 实现 Ghostwriter Protocol 思维链驱动指令集
- 重构 promptBuilder.js:实现完整的 Chain of Thought 系统提示词模板 - 新增 XML 输出解析器:parseGhostwriterOutput 和 createStreamParser - 实现动态 Few-Shot:智能裁剪参考案例防止 Token 爆炸 - 更新 DeepSeekAPI:使用 Ghostwriter Protocol 系统提示词 - 更新 appStore:分离 thinking 和 draft 内容,新增流式解析状态机 - 更新 MainContent.vue:新增 AI 思考路径折叠面板,显示风格分析与大纲规划 - 新增生成阶段指示器:thinking -> draft -> critique -> refine
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { GHOSTWRITER_SYSTEM_PROMPT } from '../utils/promptBuilder.js'
|
||||
|
||||
class DeepSeekAPI {
|
||||
constructor(config) {
|
||||
this.baseURL = config.url
|
||||
@@ -87,10 +89,11 @@ class DeepSeekAPI {
|
||||
}
|
||||
|
||||
async generateContent(prompt, onContent, options = {}) {
|
||||
// 使用 Ghostwriter Protocol 系统提示词
|
||||
return this._streamRequest([
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个资深的专业写作助手,请严格按照用户的要求进行创作。'
|
||||
content: GHOSTWRITER_SYSTEM_PROMPT
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
@@ -44,9 +44,48 @@
|
||||
<p>在左侧配置参数,点击生成开始写作</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="prose prose-invert max-w-none">
|
||||
<div v-html="renderedMarkdown"></div>
|
||||
<span v-if="isGenerating" class="inline-block w-2 h-5 bg-blue-500 ml-1 animate-pulse align-middle"></span>
|
||||
<div v-else class="space-y-4">
|
||||
<!-- AI 思考过程折叠面板 -->
|
||||
<div v-if="thinkingContent || generationStage === 'thinking'" class="bg-indigo-950/30 rounded-lg border border-indigo-500/30 overflow-hidden">
|
||||
<button
|
||||
@click="isThinkingExpanded = !isThinkingExpanded"
|
||||
class="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-indigo-900/20 transition"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-indigo-400">🧠</span>
|
||||
<span class="text-sm font-medium text-indigo-300">AI 思考路径</span>
|
||||
<span v-if="generationStage === 'thinking'" class="text-xs px-2 py-0.5 rounded bg-indigo-500/20 text-indigo-400 animate-pulse">
|
||||
分析中...
|
||||
</span>
|
||||
<span v-else class="text-xs text-slate-500">
|
||||
(风格解构 + 大纲规划)
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-indigo-400 transition-transform" :class="{ 'rotate-180': isThinkingExpanded }">▼</span>
|
||||
</button>
|
||||
<div v-show="isThinkingExpanded" class="px-4 pb-4 border-t border-indigo-500/20">
|
||||
<div class="prose prose-invert prose-sm max-w-none mt-3 text-slate-300">
|
||||
<div v-html="renderedThinking"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成阶段指示器 -->
|
||||
<div v-if="isGenerating && generationStage" class="flex items-center gap-2 text-xs text-slate-500">
|
||||
<span class="w-2 h-2 rounded-full animate-pulse" :class="{
|
||||
'bg-indigo-500': generationStage === 'thinking',
|
||||
'bg-blue-500': generationStage === 'draft',
|
||||
'bg-yellow-500': generationStage === 'critique',
|
||||
'bg-green-500': generationStage === 'refine'
|
||||
}"></span>
|
||||
<span>{{ stageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 正文内容 -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div v-html="renderedMarkdown"></div>
|
||||
<span v-if="isGenerating" class="inline-block w-2 h-5 bg-blue-500 ml-1 animate-pulse align-middle"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,6 +176,8 @@ const {
|
||||
currentPage,
|
||||
showPromptDebug,
|
||||
generatedContent,
|
||||
thinkingContent,
|
||||
generationStage,
|
||||
isGenerating,
|
||||
analysisResult,
|
||||
inputTask,
|
||||
@@ -145,6 +186,10 @@ const {
|
||||
references
|
||||
} = storeToRefs(appStore)
|
||||
|
||||
// AI 思考过程折叠状态
|
||||
import { ref } from 'vue'
|
||||
const isThinkingExpanded = ref(false)
|
||||
|
||||
// 范式定义
|
||||
const paradigms = [
|
||||
{
|
||||
@@ -214,7 +259,23 @@ const constructedPrompt = computed(() => {
|
||||
|
||||
// 渲染Markdown
|
||||
const renderedMarkdown = computed(() => {
|
||||
return marked.parse(generatedContent.value)
|
||||
return marked.parse(generatedContent.value || '')
|
||||
})
|
||||
|
||||
// 渲染 AI 思考过程
|
||||
const renderedThinking = computed(() => {
|
||||
return marked.parse(thinkingContent.value || '')
|
||||
})
|
||||
|
||||
// 生成阶段标签
|
||||
const stageLabel = computed(() => {
|
||||
const labels = {
|
||||
'thinking': '🧠 风格解构 & 大纲规划中...',
|
||||
'draft': '✍️ 正在撰写初稿...',
|
||||
'critique': '💡 AI 深度反思中...',
|
||||
'refine': '✨ 深度润色中...'
|
||||
}
|
||||
return labels[generationStage.value] || ''
|
||||
})
|
||||
|
||||
// 复制内容
|
||||
|
||||
@@ -2,7 +2,7 @@ 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'
|
||||
import { buildPrompt, createStreamParser, parseGhostwriterOutput } from '../utils/promptBuilder.js'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 页面状态
|
||||
@@ -33,8 +33,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
// 生成相关
|
||||
const isGenerating = ref(false)
|
||||
const isDeepMode = ref(false)
|
||||
const generationStage = ref('') // 'draft' | 'critique' | 'refine' | ''
|
||||
const generationStage = ref('') // 'thinking' | 'draft' | 'critique' | 'refine' | ''
|
||||
const generatedContent = ref('')
|
||||
const thinkingContent = ref('') // AI 思考过程(风格分析 + 大纲规划)
|
||||
const rawStreamBuffer = ref('') // 原始流式输出缓冲
|
||||
|
||||
// 分析相关
|
||||
const analysisText = ref('')
|
||||
@@ -103,29 +105,50 @@ export const useAppStore = defineStore('app', () => {
|
||||
console.log('Store: generateContentAction 启动', { mode: isDeepMode.value ? 'Deep' : 'Standard' })
|
||||
isGenerating.value = true
|
||||
generatedContent.value = ''
|
||||
generationStage.value = 'draft'
|
||||
thinkingContent.value = ''
|
||||
rawStreamBuffer.value = ''
|
||||
generationStage.value = 'thinking'
|
||||
|
||||
try {
|
||||
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
|
||||
const streamParser = createStreamParser()
|
||||
|
||||
// 构建初稿 Prompt
|
||||
// 构建 Prompt(XML 结构化数据)
|
||||
const taskContent = inputType.value === 'outline'
|
||||
? `核心主题:${outlinePoints.value.topic}\n目标受众:${outlinePoints.value.audience}\n关键观点:\n${outlinePoints.value.keyPoints}`
|
||||
: inputTask.value
|
||||
|
||||
const draftPrompt = buildPrompt(
|
||||
const userMessage = buildPrompt(
|
||||
taskContent,
|
||||
[...selectedTags.value, customConstraint.value].filter(Boolean),
|
||||
references.value
|
||||
)
|
||||
|
||||
console.log('Store: 生成初稿...')
|
||||
let draftContent = ''
|
||||
await api.generateContent(draftPrompt, (content) => {
|
||||
generatedContent.value += content
|
||||
draftContent += content
|
||||
console.log('Store: 开始 Ghostwriter Protocol 生成流程...')
|
||||
|
||||
// 流式接收并实时解析 XML 结构
|
||||
await api.generateContent(userMessage, (chunk) => {
|
||||
rawStreamBuffer.value += chunk
|
||||
const { section } = streamParser.process(chunk)
|
||||
|
||||
// 根据当前 section 更新 UI 状态
|
||||
if (section === 'thinking' && generationStage.value !== 'thinking') {
|
||||
generationStage.value = 'thinking'
|
||||
console.log('Store: 进入思考阶段 (Style Analysis + Planning)')
|
||||
}
|
||||
if (section === 'draft' && generationStage.value !== 'draft') {
|
||||
generationStage.value = 'draft'
|
||||
console.log('Store: 进入草稿生成阶段')
|
||||
}
|
||||
})
|
||||
|
||||
// 流式完成后,解析最终结果
|
||||
const { thinking, draft, hasStructuredOutput } = streamParser.getResult()
|
||||
console.log('Store: XML 解析完成', { hasStructuredOutput, thinkingLength: thinking.length, draftLength: draft.length })
|
||||
|
||||
thinkingContent.value = thinking
|
||||
generatedContent.value = draft
|
||||
|
||||
// 如果是深度模式,进入 Agentic Workflow
|
||||
if (isDeepMode.value) {
|
||||
// 2. 批判阶段
|
||||
@@ -133,7 +156,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
console.log('Store: 进入批判阶段...')
|
||||
generatedContent.value += '\n\n---\n\n💡 **AI 深度反思 (Critique Stage)**:\n正在分析逻辑漏洞与改进空间...\n\n'
|
||||
|
||||
const critiquePrompt = `你是一个严厉的主编。请分析以下文章的逻辑漏洞、论证强度和风格一致性。请列出3-5条具体的修改建议。不要重写文章,只提供建议。\n\n文章内容:\n${draftContent}`
|
||||
const critiquePrompt = `你是一个严厉的主编。请分析以下文章的逻辑漏洞、论证强度和风格一致性。请列出3-5条具体的修改建议。不要重写文章,只提供建议。\n\n文章内容:\n${draft}`
|
||||
|
||||
let critiqueContent = ''
|
||||
await api.generateContent(critiquePrompt, (content) => {
|
||||
@@ -146,7 +169,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
console.log('Store: 进入修正阶段...')
|
||||
generatedContent.value += '\n\n---\n\n✨ **深度润色 (Refinement Stage)**:\n正在根据反思意见重写...\n\n'
|
||||
|
||||
const refinePrompt = `你是一个专业的写作专家。请根据以下修改建议,重写这篇文章。保持原文的优秀风格,同时修复提到的问题。\n\n原文:\n${draftContent}\n\n修改建议:\n${critiqueContent}\n\n请直接输出重写后的正文:`
|
||||
const refinePrompt = `你是一个专业的写作专家。请根据以下修改建议,重写这篇文章。保持原文的优秀风格,同时修复提到的问题。\n\n原文:\n${draft}\n\n修改建议:\n${critiqueContent}\n\n请直接输出重写后的正文:`
|
||||
|
||||
await api.generateContent(refinePrompt, (content) => {
|
||||
generatedContent.value += content
|
||||
@@ -241,6 +264,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
isDeepMode,
|
||||
generationStage,
|
||||
generatedContent,
|
||||
thinkingContent,
|
||||
rawStreamBuffer,
|
||||
analysisText,
|
||||
analysisResult,
|
||||
isAnalyzing,
|
||||
|
||||
@@ -1,32 +1,166 @@
|
||||
// ============================================
|
||||
// Ghostwriter Protocol - Chain of Thought Template
|
||||
// ============================================
|
||||
|
||||
export const GHOSTWRITER_SYSTEM_PROMPT = `# Role
|
||||
你是一名世界级的"影子写手"和"文体分析专家"。你的核心能力是完美复刻给定的写作风格,并将其应用于新的主题创作。
|
||||
|
||||
# Input Data
|
||||
你将收到三部分输入:
|
||||
1. <reference_material>: 需要模仿的目标范文(可能包含多篇)。
|
||||
2. <constraints>: 必须遵守的硬性规范(格式、禁忌、字数)。
|
||||
3. <user_task>: 用户希望撰写的具体内容要求。
|
||||
|
||||
# Process (Thinking Chain)
|
||||
不要直接开始写作!你必须严格按照以下步骤进行思维推理:
|
||||
|
||||
Step 1: 【风格解构】 (Style Analysis)
|
||||
仔细阅读 <reference_material>,从以下维度提取"风格指纹":
|
||||
- 语调 (Tone): 是严肃、幽默、辛辣还是温情?
|
||||
- 句式 (Sentence Structure): 喜欢长短句结合,还是大量使用从句?是否喜欢反问?
|
||||
- 词汇 (Vocabulary): 偏向学术词汇、互联网黑话还是平实口语?
|
||||
- 结构 (Structure): 文章的起承转合是如何安排的?
|
||||
|
||||
Step 2: 【大纲构建】 (Structural Planning)
|
||||
基于 <user_task> 的要求,结合 Step 1 分析出的结构特点,规划文章大纲。
|
||||
|
||||
Step 3: 【草稿生成】 (Drafting)
|
||||
撰写正文。在这一步,你需要时刻回看 Step 1 的分析,确保每一个段落的"味道"都与参考范文一致。
|
||||
同时,必须严格遵守 <constraints> 中的所有要求。
|
||||
|
||||
Step 4: 【自我审查】 (Refinement)
|
||||
检查生成的内容:
|
||||
- 是否出现了 <constraints> 中禁止的词汇?
|
||||
- 风格是否真的像参考范文?如果不够像,请进行局部重写。
|
||||
|
||||
# Output Format
|
||||
请严格按照以下 XML 格式输出你的思考过程和最终结果:
|
||||
|
||||
<thinking>
|
||||
在此处输出你的 Step 1 风格分析 和 Step 2 大纲规划。
|
||||
简要说明你打算如何模仿这种风格。
|
||||
</thinking>
|
||||
|
||||
<draft>
|
||||
在此处输出最终的文章内容。
|
||||
注意:仅输出正文,不要包含任何"好的,这是文章"之类的废话。
|
||||
</draft>
|
||||
`
|
||||
|
||||
// 动态 Few-Shot 配置
|
||||
const MAX_REFERENCES = 3
|
||||
const MAX_REFERENCE_LENGTH = 2000
|
||||
|
||||
// 智能裁剪参考案例
|
||||
const selectRepresentativeReferences = (references) => {
|
||||
if (!references || references.length === 0) return []
|
||||
if (references.length <= MAX_REFERENCES) {
|
||||
return references.map(ref => ({
|
||||
...ref,
|
||||
content: ref.content.length > MAX_REFERENCE_LENGTH
|
||||
? ref.content.substring(0, MAX_REFERENCE_LENGTH) + '...(已截断)'
|
||||
: ref.content
|
||||
}))
|
||||
}
|
||||
// 优先选择有风格标签的、内容较长的
|
||||
const sorted = [...references].sort((a, b) => {
|
||||
const aScore = (a.styleTags?.length || 0) * 10 + Math.min(a.content.length, 500)
|
||||
const bScore = (b.styleTags?.length || 0) * 10 + Math.min(b.content.length, 500)
|
||||
return bScore - aScore
|
||||
})
|
||||
return sorted.slice(0, MAX_REFERENCES).map(ref => ({
|
||||
...ref,
|
||||
content: ref.content.length > MAX_REFERENCE_LENGTH
|
||||
? ref.content.substring(0, MAX_REFERENCE_LENGTH) + '...(已截断)'
|
||||
: ref.content
|
||||
}))
|
||||
}
|
||||
|
||||
// 构建 User Message(XML 结构化数据)
|
||||
export const buildPrompt = (task, constraints, references) => {
|
||||
let prompt = `# Role\n你是一个资深的专业写作专家。你具备极强的风格模仿能力和逻辑组织能力,能够根据提供的参考资料和风格分析,创作出高度一致的高质量文稿。\n\n`;
|
||||
const selectedRefs = selectRepresentativeReferences(references)
|
||||
|
||||
// 1. 注入规范与风格分析
|
||||
prompt += `# Instructions & Style Constraints\n`;
|
||||
prompt += `<global_rules>\n`;
|
||||
constraints.forEach(tag => prompt += `- ${tag}\n`);
|
||||
prompt += `</global_rules>\n\n`;
|
||||
|
||||
// 2. 注入参考案例 (Few-Shot)
|
||||
if (references.length > 0) {
|
||||
prompt += `# Style Reference Cases\n`;
|
||||
prompt += `请深度学习并模仿以下参考资料的语调、用词习惯、句式结构和情感色彩:\n`;
|
||||
references.forEach((ref, idx) => {
|
||||
const styleInfo = ref.styleTags && ref.styleTags.length > 0
|
||||
? `\nStyle Tags: ${ref.styleTags.join(', ')}`
|
||||
: '';
|
||||
prompt += `<case_${idx + 1} title="${ref.title}">${styleInfo}\n${ref.content}\n</case_${idx + 1}>\n\n`;
|
||||
});
|
||||
// 1. 处理参考范文
|
||||
let refText
|
||||
if (selectedRefs.length > 0) {
|
||||
refText = selectedRefs.map((ref, idx) => {
|
||||
const styleInfo = ref.styleTags?.length > 0
|
||||
? `\n[已识别风格: ${ref.styleTags.join(', ')}]`
|
||||
: ''
|
||||
return `--- 范文 ${idx + 1}: ${ref.title} ---${styleInfo}\n${ref.content}`
|
||||
}).join('\n\n')
|
||||
} else {
|
||||
refText = '无特定参考风格,请使用清晰、专业的通用风格。'
|
||||
}
|
||||
|
||||
// 3. 注入用户任务
|
||||
prompt += `# Current Writing Task\n`;
|
||||
prompt += `请基于上述所有风格约束和参考案例,执行以下写作任务:\n`;
|
||||
prompt += `<task_content>\n${task}\n</task_content>\n\n`;
|
||||
// 2. 处理约束条件
|
||||
const constraintsText = constraints.length > 0
|
||||
? constraints.map(c => `- ${c}`).join('\n')
|
||||
: '- 无特殊约束'
|
||||
|
||||
// 3. 组装 XML 结构化 User Message
|
||||
return `<reference_material>
|
||||
${refText}
|
||||
</reference_material>
|
||||
|
||||
<constraints>
|
||||
${constraintsText}
|
||||
</constraints>
|
||||
|
||||
<user_task>
|
||||
${task}
|
||||
</user_task>`
|
||||
}
|
||||
|
||||
// XML 输出解析器 - 提取 <thinking> 和 <draft> 内容
|
||||
export const parseGhostwriterOutput = (fullText) => {
|
||||
const thinkingMatch = fullText.match(/<thinking>([\s\S]*?)<\/thinking>/)
|
||||
const draftMatch = fullText.match(/<draft>([\s\S]*?)<\/draft>/)
|
||||
|
||||
prompt += `直接开始输出正文内容,无需任何开场白或解释。`;
|
||||
return {
|
||||
thinking: thinkingMatch ? thinkingMatch[1].trim() : '',
|
||||
draft: draftMatch ? draftMatch[1].trim() : fullText.trim(),
|
||||
hasStructuredOutput: !!(thinkingMatch && draftMatch)
|
||||
}
|
||||
}
|
||||
|
||||
// 实时流式解析状态机
|
||||
export const createStreamParser = () => {
|
||||
let buffer = ''
|
||||
let currentSection = 'pre' // 'pre' | 'thinking' | 'between' | 'draft' | 'post'
|
||||
|
||||
return prompt;
|
||||
return {
|
||||
// 处理新的流式内容片段
|
||||
process(chunk) {
|
||||
buffer += chunk
|
||||
|
||||
// 检测状态转换
|
||||
if (currentSection === 'pre' && buffer.includes('<thinking>')) {
|
||||
currentSection = 'thinking'
|
||||
}
|
||||
if (currentSection === 'thinking' && buffer.includes('</thinking>')) {
|
||||
currentSection = 'between'
|
||||
}
|
||||
if ((currentSection === 'between' || currentSection === 'pre') && buffer.includes('<draft>')) {
|
||||
currentSection = 'draft'
|
||||
}
|
||||
if (currentSection === 'draft' && buffer.includes('</draft>')) {
|
||||
currentSection = 'post'
|
||||
}
|
||||
|
||||
return { section: currentSection, buffer }
|
||||
},
|
||||
|
||||
// 获取最终解析结果
|
||||
getResult() {
|
||||
return parseGhostwriterOutput(buffer)
|
||||
},
|
||||
|
||||
// 获取当前 section
|
||||
getCurrentSection() {
|
||||
return currentSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const parseStreamResponse = (chunk) => {
|
||||
|
||||
Reference in New Issue
Block a user